From 18de83b2a3fc120922096b7348d6375094ae1532 Mon Sep 17 00:00:00 2001
From: Jack Hay <jack@allspice.io>
Date: Sun, 4 Jun 2023 14:57:16 -0400
Subject: [PATCH] Redesign Scoped Access Tokens (#24767)

## Changes
- Adds the following high level access scopes, each with `read` and
`write` levels:
    - `activitypub`
    - `admin` (hidden if user is not a site admin)
    - `misc`
    - `notification`
    - `organization`
    - `package`
    - `issue`
    - `repository`
    - `user`
- Adds new middleware function `tokenRequiresScopes()` in addition to
`reqToken()`
  -  `tokenRequiresScopes()` is used for each high-level api section
- _if_ a scoped token is present, checks that the required scope is
included based on the section and HTTP method
  - `reqToken()` is used for individual routes
- checks that required authentication is present (but does not check
scope levels as this will already have been handled by
`tokenRequiresScopes()`
- Adds migration to convert old scoped access tokens to the new set of
scopes
- Updates the user interface for scope selection

### User interface example
<img width="903" alt="Screen Shot 2023-05-31 at 1 56 55 PM"
src="https://github.com/go-gitea/gitea/assets/23248839/654766ec-2143-4f59-9037-3b51600e32f3">
<img width="917" alt="Screen Shot 2023-05-31 at 1 56 43 PM"
src="https://github.com/go-gitea/gitea/assets/23248839/1ad64081-012c-4a73-b393-66b30352654c">

## tokenRequiresScopes  Design Decision
- `tokenRequiresScopes()` was added to more reliably cover api routes.
For an incoming request, this function uses the given scope category
(say `AccessTokenScopeCategoryOrganization`) and the HTTP method (say
`DELETE`) and verifies that any scoped tokens in use include
`delete:organization`.
- `reqToken()` is used to enforce auth for individual routes that
require it. If a scoped token is not present for a request,
`tokenRequiresScopes()` will not return an error

## TODO
- [x] Alphabetize scope categories
- [x] Change 'public repos only' to a radio button (private vs public).
Also expand this to organizations
- [X] Disable token creation if no scopes selected. Alternatively, show
warning
- [x] `reqToken()` is missing from many `POST/DELETE` routes in the api.
`tokenRequiresScopes()` only checks that a given token has the correct
scope, `reqToken()` must be used to check that a token (or some other
auth) is present.
   -  _This should be addressed in this PR_
- [x] The migration should be reviewed very carefully in order to
minimize access changes to existing user tokens.
   - _This should be addressed in this PR_
- [x] Link to api to swagger documentation, clarify what
read/write/delete levels correspond to
- [x] Review cases where more than one scope is needed as this directly
deviates from the api definition.
   - _This should be addressed in this PR_
   - For example:
   ```go
	m.Group("/users/{username}/orgs", func() {
		m.Get("", reqToken(), org.ListUserOrgs)
		m.Get("/{org}/permissions", reqToken(), org.GetUserOrgsPermissions)
}, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryUser,
auth_model.AccessTokenScopeCategoryOrganization),
context_service.UserAssignmentAPI())
   ```

## Future improvements
- [ ] Add required scopes to swagger documentation
- [ ] Redesign `reqToken()` to be opt-out rather than opt-in
- [ ] Subdivide scopes like `repository`
- [ ] Once a token is created, if it has no scopes, we should display
text instead of an empty bullet point
- [ ] If the 'public repos only' option is selected, should read
categories be selected by default

Closes #24501
Closes #24799

Co-authored-by: Jonathan Tran <jon@allspice.io>
Co-authored-by: Kyle D <kdumontnu@gmail.com>
Co-authored-by: silverwind <me@silverwind.io>
---
 .../doc/development/oauth2-provider.en-us.md  |  71 +-
 models/auth/token.go                          |   9 +
 models/auth/token_scope.go                    | 352 +++++----
 models/auth/token_scope_test.go               | 106 +--
 models/migrations/migrations.go               |   2 +
 models/migrations/v1_20/main_test.go          |  14 +
 models/migrations/v1_20/v259.go               | 360 ++++++++++
 models/migrations/v1_20/v259_test.go          | 110 +++
 modules/context/permission.go                 |  28 +-
 options/locale/locale_en-US.ini               |  10 +-
 routers/api/v1/api.go                         | 677 ++++++++++--------
 routers/web/repo/http.go                      |   2 +-
 routers/web/user/setting/applications.go      |   2 +
 services/forms/user_form_test.go              |   8 +-
 services/lfs/locks.go                         |   9 +-
 services/lfs/server.go                        |   8 +-
 templates/user/settings/applications.tmpl     | 258 ++-----
 tests/integration/api_admin_org_test.go       |   4 +-
 tests/integration/api_admin_test.go           |  22 +-
 tests/integration/api_branch_test.go          |  16 +-
 .../api_comment_attachment_test.go            |  15 +-
 tests/integration/api_comment_test.go         |  10 +-
 tests/integration/api_gpg_keys_test.go        |   6 +-
 tests/integration/api_httpsig_test.go         |   5 +-
 .../integration/api_issue_attachment_test.go  |  10 +-
 tests/integration/api_issue_label_test.go     |   8 +-
 tests/integration/api_issue_milestone_test.go |   2 +-
 tests/integration/api_issue_pin_test.go       |   8 +-
 tests/integration/api_issue_reaction_test.go  |   4 +-
 tests/integration/api_issue_stopwatch_test.go |   8 +-
 .../api_issue_subscription_test.go            |   2 +-
 tests/integration/api_issue_test.go           |  12 +-
 .../api_issue_tracked_time_test.go            |   6 +-
 tests/integration/api_keys_test.go            |   8 +-
 tests/integration/api_notification_test.go    |   4 +-
 tests/integration/api_oauth2_apps_test.go     |   6 +-
 tests/integration/api_org_test.go             |  10 +-
 tests/integration/api_packages_npm_test.go    |   2 +-
 tests/integration/api_packages_nuget_test.go  |   2 +-
 tests/integration/api_packages_pub_test.go    |   2 +-
 tests/integration/api_packages_test.go        |   2 +-
 .../integration/api_packages_vagrant_test.go  |   2 +-
 tests/integration/api_pull_review_test.go     |   6 +-
 tests/integration/api_pull_test.go            |  12 +-
 tests/integration/api_releases_test.go        |  10 +-
 tests/integration/api_repo_archive_test.go    |   2 +-
 .../integration/api_repo_collaborator_test.go |   8 +-
 tests/integration/api_repo_edit_test.go       |   4 +-
 .../integration/api_repo_file_create_test.go  |   6 +-
 .../integration/api_repo_file_delete_test.go  |   4 +-
 tests/integration/api_repo_file_get_test.go   |   2 +-
 .../integration/api_repo_file_update_test.go  |   4 +-
 .../integration/api_repo_files_change_test.go |   4 +-
 .../api_repo_get_contents_list_test.go        |   5 +-
 .../integration/api_repo_get_contents_test.go |   5 +-
 tests/integration/api_repo_git_blobs_test.go  |   3 +-
 .../integration/api_repo_git_commits_test.go  |  19 +-
 tests/integration/api_repo_git_hook_test.go   |  18 +-
 tests/integration/api_repo_git_notes_test.go  |   3 +-
 tests/integration/api_repo_git_ref_test.go    |   3 +-
 tests/integration/api_repo_git_tags_test.go   |   4 +-
 tests/integration/api_repo_git_trees_test.go  |   3 +-
 tests/integration/api_repo_hook_test.go       |   2 +-
 .../integration/api_repo_lfs_migrate_test.go  |   2 +-
 tests/integration/api_repo_lfs_test.go        |   2 +-
 tests/integration/api_repo_raw_test.go        |   2 +-
 tests/integration/api_repo_tags_test.go       |   2 +-
 tests/integration/api_repo_teams_test.go      |   4 +-
 tests/integration/api_repo_test.go            |  36 +-
 tests/integration/api_repo_topic_test.go      |   4 +-
 tests/integration/api_team_test.go            |  14 +-
 tests/integration/api_team_user_test.go       |   2 +-
 tests/integration/api_token_test.go           | 550 +++++++++++++-
 tests/integration/api_user_email_test.go      |   4 +-
 tests/integration/api_user_follow_test.go     |   4 +-
 tests/integration/api_user_heatmap_test.go    |   3 +-
 tests/integration/api_user_info_test.go       |   3 +-
 tests/integration/api_user_org_perm_test.go   |   6 +-
 tests/integration/api_user_orgs_test.go       |   4 +-
 tests/integration/api_user_search_test.go     |   5 +-
 tests/integration/api_user_star_test.go       |  14 +-
 tests/integration/api_user_watch_test.go      |   4 +-
 tests/integration/api_wiki_test.go            |   4 +-
 tests/integration/dump_restore_test.go        |   2 +-
 tests/integration/empty_repo_test.go          |   2 +-
 tests/integration/eventsource_test.go         |   2 +-
 tests/integration/git_test.go                 |   8 +-
 tests/integration/gpg_git_test.go             |  24 +-
 tests/integration/lfs_getobject_test.go       |   8 +-
 tests/integration/migrate_test.go             |   2 +-
 tests/integration/org_count_test.go           |   2 +-
 tests/integration/org_test.go                 |   2 +-
 tests/integration/privateactivity_test.go     |   4 +-
 tests/integration/pull_merge_test.go          |   4 +-
 tests/integration/pull_status_test.go         |   2 +-
 tests/integration/pull_update_test.go         |   4 +-
 tests/integration/repo_commits_test.go        |   6 +-
 tests/integration/ssh_key_test.go             |   8 +-
 tests/integration/user_test.go                |   2 +-
 web_src/css/helpers.css                       |   1 +
 .../components/ScopedAccessTokenSelector.vue  | 138 ++++
 web_src/js/index.js                           |   2 +
 102 files changed, 2183 insertions(+), 1038 deletions(-)
 create mode 100644 models/migrations/v1_20/main_test.go
 create mode 100644 models/migrations/v1_20/v259.go
 create mode 100644 models/migrations/v1_20/v259_test.go
 create mode 100644 web_src/js/components/ScopedAccessTokenSelector.vue

diff --git a/docs/content/doc/development/oauth2-provider.en-us.md b/docs/content/doc/development/oauth2-provider.en-us.md
index 5f9960a477..03833b5ac0 100644
--- a/docs/content/doc/development/oauth2-provider.en-us.md
+++ b/docs/content/doc/development/oauth2-provider.en-us.md
@@ -44,42 +44,43 @@ To use the Authorization Code Grant as a third party application it is required
 
 ## Scopes
 
-Gitea supports the following scopes for tokens:
+Gitea supports scoped access tokens, which allow users the ability to restrict tokens to operate only on selected url routes. Scopes are grouped by high-level API routes, and further refined to the following:
 
-| Name | Description |
-| ---- | ----------- |
-| **(no scope)** | Grants read-only access to public user profile and public repositories. |
-| **repo** | Full control over all repositories. |
-| &nbsp;&nbsp;&nbsp; **repo:status** | Grants read/write access to commit status in all repositories. |
-| &nbsp;&nbsp;&nbsp; **public_repo** | Grants read/write access to public repositories only. |
-| **admin:repo_hook** | Grants access to repository hooks of all repositories. This is included in the `repo` scope. |
-| &nbsp;&nbsp;&nbsp; **write:repo_hook** | Grants read/write access to repository hooks |
-| &nbsp;&nbsp;&nbsp; **read:repo_hook** | Grants read-only access to repository hooks |
-| **admin:org** | Grants full access to organization settings |
-| &nbsp;&nbsp;&nbsp; **write:org** | Grants read/write access to organization settings |
-| &nbsp;&nbsp;&nbsp; **read:org** | Grants read-only access to organization settings |
-| **admin:public_key** | Grants full access for managing public keys |
-| &nbsp;&nbsp;&nbsp; **write:public_key** | Grant read/write access to public keys |
-| &nbsp;&nbsp;&nbsp; **read:public_key** | Grant read-only access to public keys |
-| **admin:org_hook** | Grants full access to organizational-level hooks |
-| **admin:user_hook** | Grants full access to user-level hooks |
-| **notification** | Grants full access to notifications |
-| **user** | Grants full access to user profile info |
-| &nbsp;&nbsp;&nbsp; **read:user** | Grants read access to user's profile |
-| &nbsp;&nbsp;&nbsp; **user:email** | Grants read access to user's email addresses |
-| &nbsp;&nbsp;&nbsp; **user:follow** | Grants access to follow/un-follow a user |
-| **delete_repo** | Grants access to delete repositories as an admin |
-| **package** | Grants full access to hosted packages |
-| &nbsp;&nbsp;&nbsp; **write:package** | Grants read/write access to packages |
-| &nbsp;&nbsp;&nbsp; **read:package** | Grants read access to packages |
-| &nbsp;&nbsp;&nbsp; **delete:package** | Grants delete access to packages |
-| **admin:gpg_key** | Grants full access for managing GPG keys |
-| &nbsp;&nbsp;&nbsp; **write:gpg_key** | Grants read/write access to GPG keys |
-| &nbsp;&nbsp;&nbsp; **read:gpg_key** | Grants read-only access to GPG keys |
-| **admin:application** | Grants full access to manage applications |
-| &nbsp;&nbsp;&nbsp; **write:application** | Grants read/write access for managing applications |
-| &nbsp;&nbsp;&nbsp; **read:application** | Grants read access for managing applications |
-| **sudo** | Allows to perform actions as the site admin. |
+- `read`: `GET` routes
+- `write`: `POST`, `PUT`, `PATCH`, and `DELETE` routes (in addition to `GET`)
+
+Gitea token scopes are as follows:
+
+| Name | Description                                                                                                                                      |
+| ---- |--------------------------------------------------------------------------------------------------------------------------------------------------|
+| **(no scope)** | Not supported. A scope is required even for public repositories.                                                                                 |
+| **activitypub** | `activitypub` API routes: ActivityPub related operations.                                                                                        |
+| &nbsp;&nbsp;&nbsp; **read:activitypub** | Grants read access for ActivityPub operations.                                                                                                   |
+| &nbsp;&nbsp;&nbsp; **write:activitypub** | Grants read/write/delete access for ActivityPub operations.                                                                                      |
+| **admin** | `/admin/*` API routes: Site-wide administrative operations (hidden for non-admin accounts).                                                      |
+| &nbsp;&nbsp;&nbsp; **read:admin** | Grants read access for admin operations, such as getting cron jobs or registered user emails.                                                    |
+| &nbsp;&nbsp;&nbsp; **write:admin** | Grants read/write/delete access for admin operations, such as running cron jobs or updating user accounts.                                              |                                                         |
+| **issue** | `issues/*`, `labels/*`, `milestones/*` API routes: Issue-related operations.                                                                     |
+| &nbsp;&nbsp;&nbsp; **read:issue** | Grants read access for issues operations, such as getting issue comments, issue attachments, and milestones.                                     |
+| &nbsp;&nbsp;&nbsp; **write:issue** | Grants read/write/delete access for issues operations, such as posting or editing an issue comment or attachment, and updating milestones.              |
+| **misc** | miscellaneous and settings top-level API routes.                                                                                                 |
+| &nbsp;&nbsp;&nbsp; **read:misc** | Grants read access to miscellaneous operations, such as getting label and gitignore templates.                                                   |
+| &nbsp;&nbsp;&nbsp; **write:misc** | Grants read/write/delete access to miscellaneous operations, such as markup utility operations.                                                         |
+| **notification** | `notification/*` API routes: user notification operations.                                                                                       |
+| &nbsp;&nbsp;&nbsp; **read:notification** | Grants read access to user notifications, such as which notifications users are subscribed to and read new notifications.                        |
+| &nbsp;&nbsp;&nbsp; **write:notification** | Grants read/write/delete access to user notifications, such as marking notifications as read.                                                           |
+| **organization** | `orgs/*` and `teams/*` API routes: Organization and team management operations.                                                                  |
+| &nbsp;&nbsp;&nbsp; **read:organization** | Grants read access to org and team status, such as listing all orgs a user has visibility to, teams, and team members.                           |
+| &nbsp;&nbsp;&nbsp; **write:organization** | Grants read/write/delete access to org and team status, such as creating and updating teams and updating org settings.                                  |
+| **package** | `/packages/*` API routes: Packages operations                                                                                                    |
+| &nbsp;&nbsp;&nbsp; **read:package** | Grants read access to package operations, such as reading and downloading available packages.                                                    |
+| &nbsp;&nbsp;&nbsp; **write:package** | Grants read/write/delete access to package operations. Currently the same as `read:package`.                                                            |
+| **repository** | `/repos/*` API routes except `/repos/issues/*`: Repository file, pull-request, and release operations.                                           |
+| &nbsp;&nbsp;&nbsp; **read:repository** | Grants read access to repository operations, such as getting repository files, releases, collaborators.                                          |
+| &nbsp;&nbsp;&nbsp; **write:repository** | Grants read/write/delete access to repository operations, such as getting updating repository files, creating pull requests, updating collaborators.    |
+| **user** | `/user/*` and `/users/*` API routes: User-related operations.                                                                                    |
+| &nbsp;&nbsp;&nbsp; **read:user** | Grants read access to user operations, such as getting user repo subscriptions and user settings.                                                |
+| &nbsp;&nbsp;&nbsp; **write:user** | Grants read/write/delete access to user operations, such as updating user repo subscriptions, followed users, and user settings.                        |
 
 ## Client types
 
diff --git a/models/auth/token.go b/models/auth/token.go
index 3f9f117f73..9374fe38c2 100644
--- a/models/auth/token.go
+++ b/models/auth/token.go
@@ -112,6 +112,15 @@ func NewAccessToken(t *AccessToken) error {
 	return err
 }
 
+// DisplayPublicOnly whether to display this as a public-only token.
+func (t *AccessToken) DisplayPublicOnly() bool {
+	publicOnly, err := t.Scope.PublicOnly()
+	if err != nil {
+		return false
+	}
+	return publicOnly
+}
+
 func getAccessTokenIDFromCache(token string) int64 {
 	if successfulAccessTokenCache == nil {
 		return 0
diff --git a/models/auth/token_scope.go b/models/auth/token_scope.go
index 06c89fecc2..61e684ea27 100644
--- a/models/auth/token_scope.go
+++ b/models/auth/token_scope.go
@@ -6,113 +6,122 @@ package auth
 import (
 	"fmt"
 	"strings"
+
+	"code.gitea.io/gitea/models/perm"
+)
+
+// AccessTokenScopeCategory represents the scope category for an access token
+type AccessTokenScopeCategory int
+
+const (
+	AccessTokenScopeCategoryActivityPub = iota
+	AccessTokenScopeCategoryAdmin
+	AccessTokenScopeCategoryMisc
+	AccessTokenScopeCategoryNotification
+	AccessTokenScopeCategoryOrganization
+	AccessTokenScopeCategoryPackage
+	AccessTokenScopeCategoryIssue
+	AccessTokenScopeCategoryRepository
+	AccessTokenScopeCategoryUser
+)
+
+// AllAccessTokenScopeCategories contains all access token scope categories
+var AllAccessTokenScopeCategories = []AccessTokenScopeCategory{
+	AccessTokenScopeCategoryActivityPub,
+	AccessTokenScopeCategoryAdmin,
+	AccessTokenScopeCategoryMisc,
+	AccessTokenScopeCategoryNotification,
+	AccessTokenScopeCategoryOrganization,
+	AccessTokenScopeCategoryPackage,
+	AccessTokenScopeCategoryIssue,
+	AccessTokenScopeCategoryRepository,
+	AccessTokenScopeCategoryUser,
+}
+
+// AccessTokenScopeLevel represents the access levels without a given scope category
+type AccessTokenScopeLevel int
+
+const (
+	NoAccess AccessTokenScopeLevel = iota
+	Read
+	Write
 )
 
 // AccessTokenScope represents the scope for an access token.
 type AccessTokenScope string
 
+// for all categories, write implies read
 const (
-	AccessTokenScopeAll AccessTokenScope = "all"
+	AccessTokenScopeAll        AccessTokenScope = "all"
+	AccessTokenScopePublicOnly AccessTokenScope = "public-only" // limited to public orgs/repos
 
-	AccessTokenScopeRepo       AccessTokenScope = "repo"
-	AccessTokenScopeRepoStatus AccessTokenScope = "repo:status"
-	AccessTokenScopePublicRepo AccessTokenScope = "public_repo"
+	AccessTokenScopeReadActivityPub  AccessTokenScope = "read:activitypub"
+	AccessTokenScopeWriteActivityPub AccessTokenScope = "write:activitypub"
 
-	AccessTokenScopeAdminOrg AccessTokenScope = "admin:org"
-	AccessTokenScopeWriteOrg AccessTokenScope = "write:org"
-	AccessTokenScopeReadOrg  AccessTokenScope = "read:org"
+	AccessTokenScopeReadAdmin  AccessTokenScope = "read:admin"
+	AccessTokenScopeWriteAdmin AccessTokenScope = "write:admin"
 
-	AccessTokenScopeAdminPublicKey AccessTokenScope = "admin:public_key"
-	AccessTokenScopeWritePublicKey AccessTokenScope = "write:public_key"
-	AccessTokenScopeReadPublicKey  AccessTokenScope = "read:public_key"
+	AccessTokenScopeReadMisc  AccessTokenScope = "read:misc"
+	AccessTokenScopeWriteMisc AccessTokenScope = "write:misc"
 
-	AccessTokenScopeAdminRepoHook AccessTokenScope = "admin:repo_hook"
-	AccessTokenScopeWriteRepoHook AccessTokenScope = "write:repo_hook"
-	AccessTokenScopeReadRepoHook  AccessTokenScope = "read:repo_hook"
+	AccessTokenScopeReadNotification  AccessTokenScope = "read:notification"
+	AccessTokenScopeWriteNotification AccessTokenScope = "write:notification"
 
-	AccessTokenScopeAdminOrgHook AccessTokenScope = "admin:org_hook"
+	AccessTokenScopeReadOrganization  AccessTokenScope = "read:organization"
+	AccessTokenScopeWriteOrganization AccessTokenScope = "write:organization"
 
-	AccessTokenScopeAdminUserHook AccessTokenScope = "admin:user_hook"
+	AccessTokenScopeReadPackage  AccessTokenScope = "read:package"
+	AccessTokenScopeWritePackage AccessTokenScope = "write:package"
 
-	AccessTokenScopeNotification AccessTokenScope = "notification"
+	AccessTokenScopeReadIssue  AccessTokenScope = "read:issue"
+	AccessTokenScopeWriteIssue AccessTokenScope = "write:issue"
 
-	AccessTokenScopeUser       AccessTokenScope = "user"
-	AccessTokenScopeReadUser   AccessTokenScope = "read:user"
-	AccessTokenScopeUserEmail  AccessTokenScope = "user:email"
-	AccessTokenScopeUserFollow AccessTokenScope = "user:follow"
+	AccessTokenScopeReadRepository  AccessTokenScope = "read:repository"
+	AccessTokenScopeWriteRepository AccessTokenScope = "write:repository"
 
-	AccessTokenScopeDeleteRepo AccessTokenScope = "delete_repo"
-
-	AccessTokenScopePackage       AccessTokenScope = "package"
-	AccessTokenScopeWritePackage  AccessTokenScope = "write:package"
-	AccessTokenScopeReadPackage   AccessTokenScope = "read:package"
-	AccessTokenScopeDeletePackage AccessTokenScope = "delete:package"
-
-	AccessTokenScopeAdminGPGKey AccessTokenScope = "admin:gpg_key"
-	AccessTokenScopeWriteGPGKey AccessTokenScope = "write:gpg_key"
-	AccessTokenScopeReadGPGKey  AccessTokenScope = "read:gpg_key"
-
-	AccessTokenScopeAdminApplication AccessTokenScope = "admin:application"
-	AccessTokenScopeWriteApplication AccessTokenScope = "write:application"
-	AccessTokenScopeReadApplication  AccessTokenScope = "read:application"
-
-	AccessTokenScopeSudo AccessTokenScope = "sudo"
+	AccessTokenScopeReadUser  AccessTokenScope = "read:user"
+	AccessTokenScopeWriteUser AccessTokenScope = "write:user"
 )
 
-// AccessTokenScopeBitmap represents a bitmap of access token scopes.
-type AccessTokenScopeBitmap uint64
+// accessTokenScopeBitmap represents a bitmap of access token scopes.
+type accessTokenScopeBitmap uint64
 
 // Bitmap of each scope, including the child scopes.
 const (
-	// AccessTokenScopeAllBits is the bitmap of all access token scopes, except `sudo`.
-	AccessTokenScopeAllBits AccessTokenScopeBitmap = AccessTokenScopeRepoBits |
-		AccessTokenScopeAdminOrgBits | AccessTokenScopeAdminPublicKeyBits | AccessTokenScopeAdminOrgHookBits | AccessTokenScopeAdminUserHookBits |
-		AccessTokenScopeNotificationBits | AccessTokenScopeUserBits | AccessTokenScopeDeleteRepoBits |
-		AccessTokenScopePackageBits | AccessTokenScopeAdminGPGKeyBits | AccessTokenScopeAdminApplicationBits
+	// AccessTokenScopeAllBits is the bitmap of all access token scopes
+	accessTokenScopeAllBits accessTokenScopeBitmap = accessTokenScopeWriteActivityPubBits |
+		accessTokenScopeWriteAdminBits | accessTokenScopeWriteMiscBits | accessTokenScopeWriteNotificationBits |
+		accessTokenScopeWriteOrganizationBits | accessTokenScopeWritePackageBits | accessTokenScopeWriteIssueBits |
+		accessTokenScopeWriteRepositoryBits | accessTokenScopeWriteUserBits
 
-	AccessTokenScopeRepoBits       AccessTokenScopeBitmap = 1<<iota | AccessTokenScopeRepoStatusBits | AccessTokenScopePublicRepoBits | AccessTokenScopeAdminRepoHookBits
-	AccessTokenScopeRepoStatusBits AccessTokenScopeBitmap = 1 << iota
-	AccessTokenScopePublicRepoBits AccessTokenScopeBitmap = 1 << iota
+	accessTokenScopePublicOnlyBits accessTokenScopeBitmap = 1 << iota
 
-	AccessTokenScopeAdminOrgBits AccessTokenScopeBitmap = 1<<iota | AccessTokenScopeWriteOrgBits
-	AccessTokenScopeWriteOrgBits AccessTokenScopeBitmap = 1<<iota | AccessTokenScopeReadOrgBits
-	AccessTokenScopeReadOrgBits  AccessTokenScopeBitmap = 1 << iota
+	accessTokenScopeReadActivityPubBits  accessTokenScopeBitmap = 1 << iota
+	accessTokenScopeWriteActivityPubBits accessTokenScopeBitmap = 1<<iota | accessTokenScopeReadActivityPubBits
 
-	AccessTokenScopeAdminPublicKeyBits AccessTokenScopeBitmap = 1<<iota | AccessTokenScopeWritePublicKeyBits
-	AccessTokenScopeWritePublicKeyBits AccessTokenScopeBitmap = 1<<iota | AccessTokenScopeReadPublicKeyBits
-	AccessTokenScopeReadPublicKeyBits  AccessTokenScopeBitmap = 1 << iota
+	accessTokenScopeReadAdminBits  accessTokenScopeBitmap = 1 << iota
+	accessTokenScopeWriteAdminBits accessTokenScopeBitmap = 1<<iota | accessTokenScopeReadAdminBits
 
-	AccessTokenScopeAdminRepoHookBits AccessTokenScopeBitmap = 1<<iota | AccessTokenScopeWriteRepoHookBits
-	AccessTokenScopeWriteRepoHookBits AccessTokenScopeBitmap = 1<<iota | AccessTokenScopeReadRepoHookBits
-	AccessTokenScopeReadRepoHookBits  AccessTokenScopeBitmap = 1 << iota
+	accessTokenScopeReadMiscBits  accessTokenScopeBitmap = 1 << iota
+	accessTokenScopeWriteMiscBits accessTokenScopeBitmap = 1<<iota | accessTokenScopeReadMiscBits
 
-	AccessTokenScopeAdminOrgHookBits AccessTokenScopeBitmap = 1 << iota
+	accessTokenScopeReadNotificationBits  accessTokenScopeBitmap = 1 << iota
+	accessTokenScopeWriteNotificationBits accessTokenScopeBitmap = 1<<iota | accessTokenScopeReadNotificationBits
 
-	AccessTokenScopeAdminUserHookBits AccessTokenScopeBitmap = 1 << iota
+	accessTokenScopeReadOrganizationBits  accessTokenScopeBitmap = 1 << iota
+	accessTokenScopeWriteOrganizationBits accessTokenScopeBitmap = 1<<iota | accessTokenScopeReadOrganizationBits
 
-	AccessTokenScopeNotificationBits AccessTokenScopeBitmap = 1 << iota
+	accessTokenScopeReadPackageBits  accessTokenScopeBitmap = 1 << iota
+	accessTokenScopeWritePackageBits accessTokenScopeBitmap = 1<<iota | accessTokenScopeReadPackageBits
 
-	AccessTokenScopeUserBits       AccessTokenScopeBitmap = 1<<iota | AccessTokenScopeReadUserBits | AccessTokenScopeUserEmailBits | AccessTokenScopeUserFollowBits
-	AccessTokenScopeReadUserBits   AccessTokenScopeBitmap = 1 << iota
-	AccessTokenScopeUserEmailBits  AccessTokenScopeBitmap = 1 << iota
-	AccessTokenScopeUserFollowBits AccessTokenScopeBitmap = 1 << iota
+	accessTokenScopeReadIssueBits  accessTokenScopeBitmap = 1 << iota
+	accessTokenScopeWriteIssueBits accessTokenScopeBitmap = 1<<iota | accessTokenScopeReadIssueBits
 
-	AccessTokenScopeDeleteRepoBits AccessTokenScopeBitmap = 1 << iota
+	accessTokenScopeReadRepositoryBits  accessTokenScopeBitmap = 1 << iota
+	accessTokenScopeWriteRepositoryBits accessTokenScopeBitmap = 1<<iota | accessTokenScopeReadRepositoryBits
 
-	AccessTokenScopePackageBits       AccessTokenScopeBitmap = 1<<iota | AccessTokenScopeWritePackageBits | AccessTokenScopeDeletePackageBits
-	AccessTokenScopeWritePackageBits  AccessTokenScopeBitmap = 1<<iota | AccessTokenScopeReadPackageBits
-	AccessTokenScopeReadPackageBits   AccessTokenScopeBitmap = 1 << iota
-	AccessTokenScopeDeletePackageBits AccessTokenScopeBitmap = 1 << iota
-
-	AccessTokenScopeAdminGPGKeyBits AccessTokenScopeBitmap = 1<<iota | AccessTokenScopeWriteGPGKeyBits
-	AccessTokenScopeWriteGPGKeyBits AccessTokenScopeBitmap = 1<<iota | AccessTokenScopeReadGPGKeyBits
-	AccessTokenScopeReadGPGKeyBits  AccessTokenScopeBitmap = 1 << iota
-
-	AccessTokenScopeAdminApplicationBits AccessTokenScopeBitmap = 1<<iota | AccessTokenScopeWriteApplicationBits
-	AccessTokenScopeWriteApplicationBits AccessTokenScopeBitmap = 1<<iota | AccessTokenScopeReadApplicationBits
-	AccessTokenScopeReadApplicationBits  AccessTokenScopeBitmap = 1 << iota
-
-	AccessTokenScopeSudoBits AccessTokenScopeBitmap = 1 << iota
+	accessTokenScopeReadUserBits  accessTokenScopeBitmap = 1 << iota
+	accessTokenScopeWriteUserBits accessTokenScopeBitmap = 1<<iota | accessTokenScopeReadUserBits
 
 	// The current implementation only supports up to 64 token scopes.
 	// If we need to support > 64 scopes,
@@ -120,61 +129,110 @@ const (
 )
 
 // allAccessTokenScopes contains all access token scopes.
-// The order is important: parent scope must precedes child scopes.
+// The order is important: parent scope must precede child scopes.
 var allAccessTokenScopes = []AccessTokenScope{
-	AccessTokenScopeRepo, AccessTokenScopeRepoStatus, AccessTokenScopePublicRepo,
-	AccessTokenScopeAdminOrg, AccessTokenScopeWriteOrg, AccessTokenScopeReadOrg,
-	AccessTokenScopeAdminPublicKey, AccessTokenScopeWritePublicKey, AccessTokenScopeReadPublicKey,
-	AccessTokenScopeAdminRepoHook, AccessTokenScopeWriteRepoHook, AccessTokenScopeReadRepoHook,
-	AccessTokenScopeAdminOrgHook,
-	AccessTokenScopeAdminUserHook,
-	AccessTokenScopeNotification,
-	AccessTokenScopeUser, AccessTokenScopeReadUser, AccessTokenScopeUserEmail, AccessTokenScopeUserFollow,
-	AccessTokenScopeDeleteRepo,
-	AccessTokenScopePackage, AccessTokenScopeWritePackage, AccessTokenScopeReadPackage, AccessTokenScopeDeletePackage,
-	AccessTokenScopeAdminGPGKey, AccessTokenScopeWriteGPGKey, AccessTokenScopeReadGPGKey,
-	AccessTokenScopeAdminApplication, AccessTokenScopeWriteApplication, AccessTokenScopeReadApplication,
-	AccessTokenScopeSudo,
+	AccessTokenScopePublicOnly,
+	AccessTokenScopeWriteActivityPub, AccessTokenScopeReadActivityPub,
+	AccessTokenScopeWriteAdmin, AccessTokenScopeReadAdmin,
+	AccessTokenScopeWriteMisc, AccessTokenScopeReadMisc,
+	AccessTokenScopeWriteNotification, AccessTokenScopeReadNotification,
+	AccessTokenScopeWriteOrganization, AccessTokenScopeReadOrganization,
+	AccessTokenScopeWritePackage, AccessTokenScopeReadPackage,
+	AccessTokenScopeWriteIssue, AccessTokenScopeReadIssue,
+	AccessTokenScopeWriteRepository, AccessTokenScopeReadRepository,
+	AccessTokenScopeWriteUser, AccessTokenScopeReadUser,
 }
 
 // allAccessTokenScopeBits contains all access token scopes.
-var allAccessTokenScopeBits = map[AccessTokenScope]AccessTokenScopeBitmap{
-	AccessTokenScopeRepo:             AccessTokenScopeRepoBits,
-	AccessTokenScopeRepoStatus:       AccessTokenScopeRepoStatusBits,
-	AccessTokenScopePublicRepo:       AccessTokenScopePublicRepoBits,
-	AccessTokenScopeAdminOrg:         AccessTokenScopeAdminOrgBits,
-	AccessTokenScopeWriteOrg:         AccessTokenScopeWriteOrgBits,
-	AccessTokenScopeReadOrg:          AccessTokenScopeReadOrgBits,
-	AccessTokenScopeAdminPublicKey:   AccessTokenScopeAdminPublicKeyBits,
-	AccessTokenScopeWritePublicKey:   AccessTokenScopeWritePublicKeyBits,
-	AccessTokenScopeReadPublicKey:    AccessTokenScopeReadPublicKeyBits,
-	AccessTokenScopeAdminRepoHook:    AccessTokenScopeAdminRepoHookBits,
-	AccessTokenScopeWriteRepoHook:    AccessTokenScopeWriteRepoHookBits,
-	AccessTokenScopeReadRepoHook:     AccessTokenScopeReadRepoHookBits,
-	AccessTokenScopeAdminOrgHook:     AccessTokenScopeAdminOrgHookBits,
-	AccessTokenScopeAdminUserHook:    AccessTokenScopeAdminUserHookBits,
-	AccessTokenScopeNotification:     AccessTokenScopeNotificationBits,
-	AccessTokenScopeUser:             AccessTokenScopeUserBits,
-	AccessTokenScopeReadUser:         AccessTokenScopeReadUserBits,
-	AccessTokenScopeUserEmail:        AccessTokenScopeUserEmailBits,
-	AccessTokenScopeUserFollow:       AccessTokenScopeUserFollowBits,
-	AccessTokenScopeDeleteRepo:       AccessTokenScopeDeleteRepoBits,
-	AccessTokenScopePackage:          AccessTokenScopePackageBits,
-	AccessTokenScopeWritePackage:     AccessTokenScopeWritePackageBits,
-	AccessTokenScopeReadPackage:      AccessTokenScopeReadPackageBits,
-	AccessTokenScopeDeletePackage:    AccessTokenScopeDeletePackageBits,
-	AccessTokenScopeAdminGPGKey:      AccessTokenScopeAdminGPGKeyBits,
-	AccessTokenScopeWriteGPGKey:      AccessTokenScopeWriteGPGKeyBits,
-	AccessTokenScopeReadGPGKey:       AccessTokenScopeReadGPGKeyBits,
-	AccessTokenScopeAdminApplication: AccessTokenScopeAdminApplicationBits,
-	AccessTokenScopeWriteApplication: AccessTokenScopeWriteApplicationBits,
-	AccessTokenScopeReadApplication:  AccessTokenScopeReadApplicationBits,
-	AccessTokenScopeSudo:             AccessTokenScopeSudoBits,
+var allAccessTokenScopeBits = map[AccessTokenScope]accessTokenScopeBitmap{
+	AccessTokenScopeAll:               accessTokenScopeAllBits,
+	AccessTokenScopePublicOnly:        accessTokenScopePublicOnlyBits,
+	AccessTokenScopeReadActivityPub:   accessTokenScopeReadActivityPubBits,
+	AccessTokenScopeWriteActivityPub:  accessTokenScopeWriteActivityPubBits,
+	AccessTokenScopeReadAdmin:         accessTokenScopeReadAdminBits,
+	AccessTokenScopeWriteAdmin:        accessTokenScopeWriteAdminBits,
+	AccessTokenScopeReadMisc:          accessTokenScopeReadMiscBits,
+	AccessTokenScopeWriteMisc:         accessTokenScopeWriteMiscBits,
+	AccessTokenScopeReadNotification:  accessTokenScopeReadNotificationBits,
+	AccessTokenScopeWriteNotification: accessTokenScopeWriteNotificationBits,
+	AccessTokenScopeReadOrganization:  accessTokenScopeReadOrganizationBits,
+	AccessTokenScopeWriteOrganization: accessTokenScopeWriteOrganizationBits,
+	AccessTokenScopeReadPackage:       accessTokenScopeReadPackageBits,
+	AccessTokenScopeWritePackage:      accessTokenScopeWritePackageBits,
+	AccessTokenScopeReadIssue:         accessTokenScopeReadIssueBits,
+	AccessTokenScopeWriteIssue:        accessTokenScopeWriteIssueBits,
+	AccessTokenScopeReadRepository:    accessTokenScopeReadRepositoryBits,
+	AccessTokenScopeWriteRepository:   accessTokenScopeWriteRepositoryBits,
+	AccessTokenScopeReadUser:          accessTokenScopeReadUserBits,
+	AccessTokenScopeWriteUser:         accessTokenScopeWriteUserBits,
 }
 
-// Parse parses the scope string into a bitmap, thus removing possible duplicates.
-func (s AccessTokenScope) Parse() (AccessTokenScopeBitmap, error) {
-	var bitmap AccessTokenScopeBitmap
+// readAccessTokenScopes maps a scope category to the read permission scope
+var accessTokenScopes = map[AccessTokenScopeLevel]map[AccessTokenScopeCategory]AccessTokenScope{
+	Read: {
+		AccessTokenScopeCategoryActivityPub:  AccessTokenScopeReadActivityPub,
+		AccessTokenScopeCategoryAdmin:        AccessTokenScopeReadAdmin,
+		AccessTokenScopeCategoryMisc:         AccessTokenScopeReadMisc,
+		AccessTokenScopeCategoryNotification: AccessTokenScopeReadNotification,
+		AccessTokenScopeCategoryOrganization: AccessTokenScopeReadOrganization,
+		AccessTokenScopeCategoryPackage:      AccessTokenScopeReadPackage,
+		AccessTokenScopeCategoryIssue:        AccessTokenScopeReadIssue,
+		AccessTokenScopeCategoryRepository:   AccessTokenScopeReadRepository,
+		AccessTokenScopeCategoryUser:         AccessTokenScopeReadUser,
+	},
+	Write: {
+		AccessTokenScopeCategoryActivityPub:  AccessTokenScopeWriteActivityPub,
+		AccessTokenScopeCategoryAdmin:        AccessTokenScopeWriteAdmin,
+		AccessTokenScopeCategoryMisc:         AccessTokenScopeWriteMisc,
+		AccessTokenScopeCategoryNotification: AccessTokenScopeWriteNotification,
+		AccessTokenScopeCategoryOrganization: AccessTokenScopeWriteOrganization,
+		AccessTokenScopeCategoryPackage:      AccessTokenScopeWritePackage,
+		AccessTokenScopeCategoryIssue:        AccessTokenScopeWriteIssue,
+		AccessTokenScopeCategoryRepository:   AccessTokenScopeWriteRepository,
+		AccessTokenScopeCategoryUser:         AccessTokenScopeWriteUser,
+	},
+}
+
+// GetRequiredScopes gets the specific scopes for a given level and categories
+func GetRequiredScopes(level AccessTokenScopeLevel, scopeCategories ...AccessTokenScopeCategory) []AccessTokenScope {
+	scopes := make([]AccessTokenScope, 0, len(scopeCategories))
+	for _, cat := range scopeCategories {
+		scopes = append(scopes, accessTokenScopes[level][cat])
+	}
+	return scopes
+}
+
+// ContainsCategory checks if a list of categories contains a specific category
+func ContainsCategory(categories []AccessTokenScopeCategory, category AccessTokenScopeCategory) bool {
+	for _, c := range categories {
+		if c == category {
+			return true
+		}
+	}
+	return false
+}
+
+// GetScopeLevelFromAccessMode converts permission access mode to scope level
+func GetScopeLevelFromAccessMode(mode perm.AccessMode) AccessTokenScopeLevel {
+	switch mode {
+	case perm.AccessModeNone:
+		return NoAccess
+	case perm.AccessModeRead:
+		return Read
+	case perm.AccessModeWrite:
+		return Write
+	case perm.AccessModeAdmin:
+		return Write
+	case perm.AccessModeOwner:
+		return Write
+	default:
+		return NoAccess
+	}
+}
+
+// parse the scope string into a bitmap, thus removing possible duplicates.
+func (s AccessTokenScope) parse() (accessTokenScopeBitmap, error) {
+	var bitmap accessTokenScopeBitmap
 
 	// The following is the more performant equivalent of 'for _, v := range strings.Split(remainingScope, ",")' as this is hot code
 	remainingScopes := string(s)
@@ -196,7 +254,7 @@ func (s AccessTokenScope) Parse() (AccessTokenScopeBitmap, error) {
 			continue
 		}
 		if singleScope == AccessTokenScopeAll {
-			bitmap |= AccessTokenScopeAllBits
+			bitmap |= accessTokenScopeAllBits
 			continue
 		}
 
@@ -217,26 +275,42 @@ func (s AccessTokenScope) StringSlice() []string {
 
 // Normalize returns a normalized scope string without any duplicates.
 func (s AccessTokenScope) Normalize() (AccessTokenScope, error) {
-	bitmap, err := s.Parse()
+	bitmap, err := s.parse()
 	if err != nil {
 		return "", err
 	}
 
-	return bitmap.ToScope(), nil
+	return bitmap.toScope(), nil
 }
 
-// HasScope returns true if the string has the given scope
-func (s AccessTokenScope) HasScope(scope AccessTokenScope) (bool, error) {
-	bitmap, err := s.Parse()
+// PublicOnly checks if this token scope is limited to public resources
+func (s AccessTokenScope) PublicOnly() (bool, error) {
+	bitmap, err := s.parse()
 	if err != nil {
 		return false, err
 	}
 
-	return bitmap.HasScope(scope)
+	return bitmap.hasScope(AccessTokenScopePublicOnly)
 }
 
 // HasScope returns true if the string has the given scope
-func (bitmap AccessTokenScopeBitmap) HasScope(scope AccessTokenScope) (bool, error) {
+func (s AccessTokenScope) HasScope(scopes ...AccessTokenScope) (bool, error) {
+	bitmap, err := s.parse()
+	if err != nil {
+		return false, err
+	}
+
+	for _, s := range scopes {
+		if has, err := bitmap.hasScope(s); !has || err != nil {
+			return has, err
+		}
+	}
+
+	return true, nil
+}
+
+// hasScope returns true if the string has the given scope
+func (bitmap accessTokenScopeBitmap) hasScope(scope AccessTokenScope) (bool, error) {
 	expectedBits, ok := allAccessTokenScopeBits[scope]
 	if !ok {
 		return false, fmt.Errorf("invalid access token scope: %s", scope)
@@ -245,17 +319,17 @@ func (bitmap AccessTokenScopeBitmap) HasScope(scope AccessTokenScope) (bool, err
 	return bitmap&expectedBits == expectedBits, nil
 }
 
-// ToScope returns a normalized scope string without any duplicates.
-func (bitmap AccessTokenScopeBitmap) ToScope() AccessTokenScope {
+// toScope returns a normalized scope string without any duplicates.
+func (bitmap accessTokenScopeBitmap) toScope() AccessTokenScope {
 	var scopes []string
 
 	// iterate over all scopes, and reconstruct the bitmap
 	// if the reconstructed bitmap doesn't change, then the scope is already included
-	var reconstruct AccessTokenScopeBitmap
+	var reconstruct accessTokenScopeBitmap
 
 	for _, singleScope := range allAccessTokenScopes {
 		// no need for error checking here, since we know the scope is valid
-		if ok, _ := bitmap.HasScope(singleScope); ok {
+		if ok, _ := bitmap.hasScope(singleScope); ok {
 			current := reconstruct | allAccessTokenScopeBits[singleScope]
 			if current == reconstruct {
 				continue
@@ -269,7 +343,7 @@ func (bitmap AccessTokenScopeBitmap) ToScope() AccessTokenScope {
 	scope := AccessTokenScope(strings.Join(scopes, ","))
 	scope = AccessTokenScope(strings.ReplaceAll(
 		string(scope),
-		"repo,admin:org,admin:public_key,admin:org_hook,admin:user_hook,notification,user,delete_repo,package,admin:gpg_key,admin:application",
+		"write:activitypub,write:admin,write:misc,write:notification,write:organization,write:package,write:issue,write:repository,write:user",
 		"all",
 	))
 	return scope
diff --git a/models/auth/token_scope_test.go b/models/auth/token_scope_test.go
index b96a5fd469..a6097e45d7 100644
--- a/models/auth/token_scope_test.go
+++ b/models/auth/token_scope_test.go
@@ -4,44 +4,35 @@
 package auth
 
 import (
+	"fmt"
 	"testing"
 
 	"github.com/stretchr/testify/assert"
 )
 
+type scopeTestNormalize struct {
+	in  AccessTokenScope
+	out AccessTokenScope
+	err error
+}
+
 func TestAccessTokenScope_Normalize(t *testing.T) {
-	tests := []struct {
-		in  AccessTokenScope
-		out AccessTokenScope
-		err error
-	}{
+	tests := []scopeTestNormalize{
 		{"", "", nil},
-		{"repo", "repo", nil},
-		{"repo,repo:status", "repo", nil},
-		{"repo,public_repo", "repo", nil},
-		{"admin:public_key,write:public_key", "admin:public_key", nil},
-		{"admin:public_key,read:public_key", "admin:public_key", nil},
-		{"write:public_key,read:public_key", "write:public_key", nil}, // read is include in write
-		{"admin:repo_hook,write:repo_hook", "admin:repo_hook", nil},
-		{"admin:repo_hook,read:repo_hook", "admin:repo_hook", nil},
-		{"repo,admin:repo_hook,read:repo_hook", "repo", nil}, // admin:repo_hook is a child scope of repo
-		{"repo,read:repo_hook", "repo", nil},                 // read:repo_hook is a child scope of repo
-		{"user", "user", nil},
-		{"user,read:user", "user", nil},
-		{"user,admin:org,write:org", "admin:org,user", nil},
-		{"admin:org,write:org,user", "admin:org,user", nil},
-		{"package", "package", nil},
-		{"package,write:package", "package", nil},
-		{"package,write:package,delete:package", "package", nil},
-		{"write:package,read:package", "write:package", nil},                  // read is include in write
-		{"write:package,delete:package", "write:package,delete:package", nil}, // write and delete are not include in each other
-		{"admin:gpg_key", "admin:gpg_key", nil},
-		{"admin:gpg_key,write:gpg_key", "admin:gpg_key", nil},
-		{"admin:gpg_key,write:gpg_key,user", "user,admin:gpg_key", nil},
-		{"admin:application,write:application,user", "user,admin:application", nil},
+		{"write:misc,write:notification,read:package,write:notification,public-only", "public-only,write:misc,write:notification,read:package", nil},
 		{"all", "all", nil},
-		{"repo,admin:org,admin:public_key,admin:repo_hook,admin:org_hook,admin:user_hook,notification,user,delete_repo,package,admin:gpg_key,admin:application", "all", nil},
-		{"repo,admin:org,admin:public_key,admin:repo_hook,admin:org_hook,admin:user_hook,notification,user,delete_repo,package,admin:gpg_key,admin:application,sudo", "all,sudo", nil},
+		{"write:activitypub,write:admin,write:misc,write:notification,write:organization,write:package,write:issue,write:repository,write:user", "all", nil},
+		{"write:activitypub,write:admin,write:misc,write:notification,write:organization,write:package,write:issue,write:repository,write:user,public-only", "public-only,all", nil},
+	}
+
+	for _, scope := range []string{"activitypub", "admin", "misc", "notification", "organization", "package", "issue", "repository", "user"} {
+		tests = append(tests,
+			scopeTestNormalize{AccessTokenScope(fmt.Sprintf("read:%s", scope)), AccessTokenScope(fmt.Sprintf("read:%s", scope)), nil},
+			scopeTestNormalize{AccessTokenScope(fmt.Sprintf("write:%s", scope)), AccessTokenScope(fmt.Sprintf("write:%s", scope)), nil},
+			scopeTestNormalize{AccessTokenScope(fmt.Sprintf("write:%[1]s,read:%[1]s", scope)), AccessTokenScope(fmt.Sprintf("write:%s", scope)), nil},
+			scopeTestNormalize{AccessTokenScope(fmt.Sprintf("read:%[1]s,write:%[1]s", scope)), AccessTokenScope(fmt.Sprintf("write:%s", scope)), nil},
+			scopeTestNormalize{AccessTokenScope(fmt.Sprintf("read:%[1]s,write:%[1]s,write:%[1]s", scope)), AccessTokenScope(fmt.Sprintf("write:%s", scope)), nil},
+		)
 	}
 
 	for _, test := range tests {
@@ -53,31 +44,46 @@ func TestAccessTokenScope_Normalize(t *testing.T) {
 	}
 }
 
+type scopeTestHasScope struct {
+	in    AccessTokenScope
+	scope AccessTokenScope
+	out   bool
+	err   error
+}
+
 func TestAccessTokenScope_HasScope(t *testing.T) {
-	tests := []struct {
-		in    AccessTokenScope
-		scope AccessTokenScope
-		out   bool
-		err   error
-	}{
-		{"repo", "repo", true, nil},
-		{"repo", "repo:status", true, nil},
-		{"repo", "public_repo", true, nil},
-		{"repo", "admin:org", false, nil},
-		{"repo", "admin:public_key", false, nil},
-		{"repo:status", "repo", false, nil},
-		{"repo:status", "public_repo", false, nil},
-		{"admin:org", "write:org", true, nil},
-		{"admin:org", "read:org", true, nil},
-		{"admin:org", "admin:org", true, nil},
-		{"user", "read:user", true, nil},
-		{"package", "write:package", true, nil},
+	tests := []scopeTestHasScope{
+		{"read:admin", "write:package", false, nil},
+		{"all", "write:package", true, nil},
+		{"write:package", "all", false, nil},
+		{"public-only", "read:issue", false, nil},
+	}
+
+	for _, scope := range []string{"activitypub", "admin", "misc", "notification", "organization", "package", "issue", "repository", "user"} {
+		tests = append(tests,
+			scopeTestHasScope{
+				AccessTokenScope(fmt.Sprintf("read:%s", scope)),
+				AccessTokenScope(fmt.Sprintf("read:%s", scope)), true, nil,
+			},
+			scopeTestHasScope{
+				AccessTokenScope(fmt.Sprintf("write:%s", scope)),
+				AccessTokenScope(fmt.Sprintf("write:%s", scope)), true, nil,
+			},
+			scopeTestHasScope{
+				AccessTokenScope(fmt.Sprintf("write:%s", scope)),
+				AccessTokenScope(fmt.Sprintf("read:%s", scope)), true, nil,
+			},
+			scopeTestHasScope{
+				AccessTokenScope(fmt.Sprintf("read:%s", scope)),
+				AccessTokenScope(fmt.Sprintf("write:%s", scope)), false, nil,
+			},
+		)
 	}
 
 	for _, test := range tests {
 		t.Run(string(test.in), func(t *testing.T) {
-			scope, err := test.in.HasScope(test.scope)
-			assert.Equal(t, test.out, scope)
+			hasScope, err := test.in.HasScope(test.scope)
+			assert.Equal(t, test.out, hasScope)
 			assert.Equal(t, test.err, err)
 		})
 	}
diff --git a/models/migrations/migrations.go b/models/migrations/migrations.go
index 231c93cc74..d96c17bfb5 100644
--- a/models/migrations/migrations.go
+++ b/models/migrations/migrations.go
@@ -495,6 +495,8 @@ var migrations = []Migration{
 	NewMigration("Add Actions Artifact table", v1_20.CreateActionArtifactTable),
 	// v258 -> 259
 	NewMigration("Add PinOrder Column", v1_20.AddPinOrderToIssue),
+	// v259 -> 260
+	NewMigration("Convert scoped access tokens", v1_20.ConvertScopedAccessTokens),
 }
 
 // GetCurrentDBVersion returns the current db version
diff --git a/models/migrations/v1_20/main_test.go b/models/migrations/v1_20/main_test.go
new file mode 100644
index 0000000000..92a1a9f622
--- /dev/null
+++ b/models/migrations/v1_20/main_test.go
@@ -0,0 +1,14 @@
+// Copyright 2023 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package v1_20 //nolint
+
+import (
+	"testing"
+
+	"code.gitea.io/gitea/models/migrations/base"
+)
+
+func TestMain(m *testing.M) {
+	base.MainTest(m)
+}
diff --git a/models/migrations/v1_20/v259.go b/models/migrations/v1_20/v259.go
new file mode 100644
index 0000000000..5b8ced4ad7
--- /dev/null
+++ b/models/migrations/v1_20/v259.go
@@ -0,0 +1,360 @@
+// Copyright 2023 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package v1_20 //nolint
+
+import (
+	"fmt"
+	"strings"
+
+	"code.gitea.io/gitea/modules/log"
+
+	"xorm.io/xorm"
+)
+
+// unknownAccessTokenScope represents the scope for an access token that isn't
+// known be an old token or a new token.
+type unknownAccessTokenScope string
+
+// AccessTokenScope represents the scope for an access token.
+type AccessTokenScope string
+
+// for all categories, write implies read
+const (
+	AccessTokenScopeAll        AccessTokenScope = "all"
+	AccessTokenScopePublicOnly AccessTokenScope = "public-only" // limited to public orgs/repos
+
+	AccessTokenScopeReadActivityPub  AccessTokenScope = "read:activitypub"
+	AccessTokenScopeWriteActivityPub AccessTokenScope = "write:activitypub"
+
+	AccessTokenScopeReadAdmin  AccessTokenScope = "read:admin"
+	AccessTokenScopeWriteAdmin AccessTokenScope = "write:admin"
+
+	AccessTokenScopeReadMisc  AccessTokenScope = "read:misc"
+	AccessTokenScopeWriteMisc AccessTokenScope = "write:misc"
+
+	AccessTokenScopeReadNotification  AccessTokenScope = "read:notification"
+	AccessTokenScopeWriteNotification AccessTokenScope = "write:notification"
+
+	AccessTokenScopeReadOrganization  AccessTokenScope = "read:organization"
+	AccessTokenScopeWriteOrganization AccessTokenScope = "write:organization"
+
+	AccessTokenScopeReadPackage  AccessTokenScope = "read:package"
+	AccessTokenScopeWritePackage AccessTokenScope = "write:package"
+
+	AccessTokenScopeReadIssue  AccessTokenScope = "read:issue"
+	AccessTokenScopeWriteIssue AccessTokenScope = "write:issue"
+
+	AccessTokenScopeReadRepository  AccessTokenScope = "read:repository"
+	AccessTokenScopeWriteRepository AccessTokenScope = "write:repository"
+
+	AccessTokenScopeReadUser  AccessTokenScope = "read:user"
+	AccessTokenScopeWriteUser AccessTokenScope = "write:user"
+)
+
+// accessTokenScopeBitmap represents a bitmap of access token scopes.
+type accessTokenScopeBitmap uint64
+
+// Bitmap of each scope, including the child scopes.
+const (
+	// AccessTokenScopeAllBits is the bitmap of all access token scopes
+	accessTokenScopeAllBits accessTokenScopeBitmap = accessTokenScopeWriteActivityPubBits |
+		accessTokenScopeWriteAdminBits | accessTokenScopeWriteMiscBits | accessTokenScopeWriteNotificationBits |
+		accessTokenScopeWriteOrganizationBits | accessTokenScopeWritePackageBits | accessTokenScopeWriteIssueBits |
+		accessTokenScopeWriteRepositoryBits | accessTokenScopeWriteUserBits
+
+	accessTokenScopePublicOnlyBits accessTokenScopeBitmap = 1 << iota
+
+	accessTokenScopeReadActivityPubBits  accessTokenScopeBitmap = 1 << iota
+	accessTokenScopeWriteActivityPubBits accessTokenScopeBitmap = 1<<iota | accessTokenScopeReadActivityPubBits
+
+	accessTokenScopeReadAdminBits  accessTokenScopeBitmap = 1 << iota
+	accessTokenScopeWriteAdminBits accessTokenScopeBitmap = 1<<iota | accessTokenScopeReadAdminBits
+
+	accessTokenScopeReadMiscBits  accessTokenScopeBitmap = 1 << iota
+	accessTokenScopeWriteMiscBits accessTokenScopeBitmap = 1<<iota | accessTokenScopeReadMiscBits
+
+	accessTokenScopeReadNotificationBits  accessTokenScopeBitmap = 1 << iota
+	accessTokenScopeWriteNotificationBits accessTokenScopeBitmap = 1<<iota | accessTokenScopeReadNotificationBits
+
+	accessTokenScopeReadOrganizationBits  accessTokenScopeBitmap = 1 << iota
+	accessTokenScopeWriteOrganizationBits accessTokenScopeBitmap = 1<<iota | accessTokenScopeReadOrganizationBits
+
+	accessTokenScopeReadPackageBits  accessTokenScopeBitmap = 1 << iota
+	accessTokenScopeWritePackageBits accessTokenScopeBitmap = 1<<iota | accessTokenScopeReadPackageBits
+
+	accessTokenScopeReadIssueBits  accessTokenScopeBitmap = 1 << iota
+	accessTokenScopeWriteIssueBits accessTokenScopeBitmap = 1<<iota | accessTokenScopeReadIssueBits
+
+	accessTokenScopeReadRepositoryBits  accessTokenScopeBitmap = 1 << iota
+	accessTokenScopeWriteRepositoryBits accessTokenScopeBitmap = 1<<iota | accessTokenScopeReadRepositoryBits
+
+	accessTokenScopeReadUserBits  accessTokenScopeBitmap = 1 << iota
+	accessTokenScopeWriteUserBits accessTokenScopeBitmap = 1<<iota | accessTokenScopeReadUserBits
+
+	// The current implementation only supports up to 64 token scopes.
+	// If we need to support > 64 scopes,
+	// refactoring the whole implementation in this file (and only this file) is needed.
+)
+
+// allAccessTokenScopes contains all access token scopes.
+// The order is important: parent scope must precede child scopes.
+var allAccessTokenScopes = []AccessTokenScope{
+	AccessTokenScopePublicOnly,
+	AccessTokenScopeWriteActivityPub, AccessTokenScopeReadActivityPub,
+	AccessTokenScopeWriteAdmin, AccessTokenScopeReadAdmin,
+	AccessTokenScopeWriteMisc, AccessTokenScopeReadMisc,
+	AccessTokenScopeWriteNotification, AccessTokenScopeReadNotification,
+	AccessTokenScopeWriteOrganization, AccessTokenScopeReadOrganization,
+	AccessTokenScopeWritePackage, AccessTokenScopeReadPackage,
+	AccessTokenScopeWriteIssue, AccessTokenScopeReadIssue,
+	AccessTokenScopeWriteRepository, AccessTokenScopeReadRepository,
+	AccessTokenScopeWriteUser, AccessTokenScopeReadUser,
+}
+
+// allAccessTokenScopeBits contains all access token scopes.
+var allAccessTokenScopeBits = map[AccessTokenScope]accessTokenScopeBitmap{
+	AccessTokenScopeAll:               accessTokenScopeAllBits,
+	AccessTokenScopePublicOnly:        accessTokenScopePublicOnlyBits,
+	AccessTokenScopeReadActivityPub:   accessTokenScopeReadActivityPubBits,
+	AccessTokenScopeWriteActivityPub:  accessTokenScopeWriteActivityPubBits,
+	AccessTokenScopeReadAdmin:         accessTokenScopeReadAdminBits,
+	AccessTokenScopeWriteAdmin:        accessTokenScopeWriteAdminBits,
+	AccessTokenScopeReadMisc:          accessTokenScopeReadMiscBits,
+	AccessTokenScopeWriteMisc:         accessTokenScopeWriteMiscBits,
+	AccessTokenScopeReadNotification:  accessTokenScopeReadNotificationBits,
+	AccessTokenScopeWriteNotification: accessTokenScopeWriteNotificationBits,
+	AccessTokenScopeReadOrganization:  accessTokenScopeReadOrganizationBits,
+	AccessTokenScopeWriteOrganization: accessTokenScopeWriteOrganizationBits,
+	AccessTokenScopeReadPackage:       accessTokenScopeReadPackageBits,
+	AccessTokenScopeWritePackage:      accessTokenScopeWritePackageBits,
+	AccessTokenScopeReadIssue:         accessTokenScopeReadIssueBits,
+	AccessTokenScopeWriteIssue:        accessTokenScopeWriteIssueBits,
+	AccessTokenScopeReadRepository:    accessTokenScopeReadRepositoryBits,
+	AccessTokenScopeWriteRepository:   accessTokenScopeWriteRepositoryBits,
+	AccessTokenScopeReadUser:          accessTokenScopeReadUserBits,
+	AccessTokenScopeWriteUser:         accessTokenScopeWriteUserBits,
+}
+
+// hasScope returns true if the string has the given scope
+func (bitmap accessTokenScopeBitmap) hasScope(scope AccessTokenScope) (bool, error) {
+	expectedBits, ok := allAccessTokenScopeBits[scope]
+	if !ok {
+		return false, fmt.Errorf("invalid access token scope: %s", scope)
+	}
+
+	return bitmap&expectedBits == expectedBits, nil
+}
+
+// toScope returns a normalized scope string without any duplicates.
+func (bitmap accessTokenScopeBitmap) toScope(unknownScopes *[]unknownAccessTokenScope) AccessTokenScope {
+	var scopes []string
+
+	// Preserve unknown scopes, and put them at the beginning so that it's clear
+	// when debugging.
+	if unknownScopes != nil {
+		for _, unknownScope := range *unknownScopes {
+			scopes = append(scopes, string(unknownScope))
+		}
+	}
+
+	// iterate over all scopes, and reconstruct the bitmap
+	// if the reconstructed bitmap doesn't change, then the scope is already included
+	var reconstruct accessTokenScopeBitmap
+
+	for _, singleScope := range allAccessTokenScopes {
+		// no need for error checking here, since we know the scope is valid
+		if ok, _ := bitmap.hasScope(singleScope); ok {
+			current := reconstruct | allAccessTokenScopeBits[singleScope]
+			if current == reconstruct {
+				continue
+			}
+
+			reconstruct = current
+			scopes = append(scopes, string(singleScope))
+		}
+	}
+
+	scope := AccessTokenScope(strings.Join(scopes, ","))
+	scope = AccessTokenScope(strings.ReplaceAll(
+		string(scope),
+		"write:activitypub,write:admin,write:misc,write:notification,write:organization,write:package,write:issue,write:repository,write:user",
+		"all",
+	))
+	return scope
+}
+
+// parse the scope string into a bitmap, thus removing possible duplicates.
+func (s AccessTokenScope) parse() (accessTokenScopeBitmap, *[]unknownAccessTokenScope) {
+	var bitmap accessTokenScopeBitmap
+	var unknownScopes []unknownAccessTokenScope
+
+	// The following is the more performant equivalent of 'for _, v := range strings.Split(remainingScope, ",")' as this is hot code
+	remainingScopes := string(s)
+	for len(remainingScopes) > 0 {
+		i := strings.IndexByte(remainingScopes, ',')
+		var v string
+		if i < 0 {
+			v = remainingScopes
+			remainingScopes = ""
+		} else if i+1 >= len(remainingScopes) {
+			v = remainingScopes[:i]
+			remainingScopes = ""
+		} else {
+			v = remainingScopes[:i]
+			remainingScopes = remainingScopes[i+1:]
+		}
+		singleScope := AccessTokenScope(v)
+		if singleScope == "" {
+			continue
+		}
+		if singleScope == AccessTokenScopeAll {
+			bitmap |= accessTokenScopeAllBits
+			continue
+		}
+
+		bits, ok := allAccessTokenScopeBits[singleScope]
+		if !ok {
+			unknownScopes = append(unknownScopes, unknownAccessTokenScope(string(singleScope)))
+		}
+		bitmap |= bits
+	}
+
+	return bitmap, &unknownScopes
+}
+
+// NormalizePreservingUnknown returns a normalized scope string without any
+// duplicates.  Unknown scopes are included.
+func (s AccessTokenScope) NormalizePreservingUnknown() AccessTokenScope {
+	bitmap, unknownScopes := s.parse()
+
+	return bitmap.toScope(unknownScopes)
+}
+
+// OldAccessTokenScope represents the scope for an access token.
+type OldAccessTokenScope string
+
+const (
+	OldAccessTokenScopeAll OldAccessTokenScope = "all"
+
+	OldAccessTokenScopeRepo       OldAccessTokenScope = "repo"
+	OldAccessTokenScopeRepoStatus OldAccessTokenScope = "repo:status"
+	OldAccessTokenScopePublicRepo OldAccessTokenScope = "public_repo"
+
+	OldAccessTokenScopeAdminOrg OldAccessTokenScope = "admin:org"
+	OldAccessTokenScopeWriteOrg OldAccessTokenScope = "write:org"
+	OldAccessTokenScopeReadOrg  OldAccessTokenScope = "read:org"
+
+	OldAccessTokenScopeAdminPublicKey OldAccessTokenScope = "admin:public_key"
+	OldAccessTokenScopeWritePublicKey OldAccessTokenScope = "write:public_key"
+	OldAccessTokenScopeReadPublicKey  OldAccessTokenScope = "read:public_key"
+
+	OldAccessTokenScopeAdminRepoHook OldAccessTokenScope = "admin:repo_hook"
+	OldAccessTokenScopeWriteRepoHook OldAccessTokenScope = "write:repo_hook"
+	OldAccessTokenScopeReadRepoHook  OldAccessTokenScope = "read:repo_hook"
+
+	OldAccessTokenScopeAdminOrgHook OldAccessTokenScope = "admin:org_hook"
+
+	OldAccessTokenScopeNotification OldAccessTokenScope = "notification"
+
+	OldAccessTokenScopeUser       OldAccessTokenScope = "user"
+	OldAccessTokenScopeReadUser   OldAccessTokenScope = "read:user"
+	OldAccessTokenScopeUserEmail  OldAccessTokenScope = "user:email"
+	OldAccessTokenScopeUserFollow OldAccessTokenScope = "user:follow"
+
+	OldAccessTokenScopeDeleteRepo OldAccessTokenScope = "delete_repo"
+
+	OldAccessTokenScopePackage       OldAccessTokenScope = "package"
+	OldAccessTokenScopeWritePackage  OldAccessTokenScope = "write:package"
+	OldAccessTokenScopeReadPackage   OldAccessTokenScope = "read:package"
+	OldAccessTokenScopeDeletePackage OldAccessTokenScope = "delete:package"
+
+	OldAccessTokenScopeAdminGPGKey OldAccessTokenScope = "admin:gpg_key"
+	OldAccessTokenScopeWriteGPGKey OldAccessTokenScope = "write:gpg_key"
+	OldAccessTokenScopeReadGPGKey  OldAccessTokenScope = "read:gpg_key"
+
+	OldAccessTokenScopeAdminApplication OldAccessTokenScope = "admin:application"
+	OldAccessTokenScopeWriteApplication OldAccessTokenScope = "write:application"
+	OldAccessTokenScopeReadApplication  OldAccessTokenScope = "read:application"
+
+	OldAccessTokenScopeSudo OldAccessTokenScope = "sudo"
+)
+
+var accessTokenScopeMap = map[OldAccessTokenScope][]AccessTokenScope{
+	OldAccessTokenScopeAll:              {AccessTokenScopeAll},
+	OldAccessTokenScopeRepo:             {AccessTokenScopeWriteRepository},
+	OldAccessTokenScopeRepoStatus:       {AccessTokenScopeWriteRepository},
+	OldAccessTokenScopePublicRepo:       {AccessTokenScopePublicOnly, AccessTokenScopeWriteRepository},
+	OldAccessTokenScopeAdminOrg:         {AccessTokenScopeWriteOrganization},
+	OldAccessTokenScopeWriteOrg:         {AccessTokenScopeWriteOrganization},
+	OldAccessTokenScopeReadOrg:          {AccessTokenScopeReadOrganization},
+	OldAccessTokenScopeAdminPublicKey:   {AccessTokenScopeWriteUser},
+	OldAccessTokenScopeWritePublicKey:   {AccessTokenScopeWriteUser},
+	OldAccessTokenScopeReadPublicKey:    {AccessTokenScopeReadUser},
+	OldAccessTokenScopeAdminRepoHook:    {AccessTokenScopeWriteRepository},
+	OldAccessTokenScopeWriteRepoHook:    {AccessTokenScopeWriteRepository},
+	OldAccessTokenScopeReadRepoHook:     {AccessTokenScopeReadRepository},
+	OldAccessTokenScopeAdminOrgHook:     {AccessTokenScopeWriteOrganization},
+	OldAccessTokenScopeNotification:     {AccessTokenScopeWriteNotification},
+	OldAccessTokenScopeUser:             {AccessTokenScopeWriteUser},
+	OldAccessTokenScopeReadUser:         {AccessTokenScopeReadUser},
+	OldAccessTokenScopeUserEmail:        {AccessTokenScopeWriteUser},
+	OldAccessTokenScopeUserFollow:       {AccessTokenScopeWriteUser},
+	OldAccessTokenScopeDeleteRepo:       {AccessTokenScopeWriteRepository},
+	OldAccessTokenScopePackage:          {AccessTokenScopeWritePackage},
+	OldAccessTokenScopeWritePackage:     {AccessTokenScopeWritePackage},
+	OldAccessTokenScopeReadPackage:      {AccessTokenScopeReadPackage},
+	OldAccessTokenScopeDeletePackage:    {AccessTokenScopeWritePackage},
+	OldAccessTokenScopeAdminGPGKey:      {AccessTokenScopeWriteUser},
+	OldAccessTokenScopeWriteGPGKey:      {AccessTokenScopeWriteUser},
+	OldAccessTokenScopeReadGPGKey:       {AccessTokenScopeReadUser},
+	OldAccessTokenScopeAdminApplication: {AccessTokenScopeWriteUser},
+	OldAccessTokenScopeWriteApplication: {AccessTokenScopeWriteUser},
+	OldAccessTokenScopeReadApplication:  {AccessTokenScopeReadUser},
+	OldAccessTokenScopeSudo:             {AccessTokenScopeWriteAdmin},
+}
+
+type AccessToken struct {
+	ID    int64 `xorm:"pk autoincr"`
+	Scope string
+}
+
+func ConvertScopedAccessTokens(x *xorm.Engine) error {
+	var tokens []*AccessToken
+
+	if err := x.Find(&tokens); err != nil {
+		return err
+	}
+
+	for _, token := range tokens {
+		var scopes []string
+		allNewScopesMap := make(map[AccessTokenScope]bool)
+		for _, oldScope := range strings.Split(token.Scope, ",") {
+			if newScopes, exists := accessTokenScopeMap[OldAccessTokenScope(oldScope)]; exists {
+				for _, newScope := range newScopes {
+					allNewScopesMap[newScope] = true
+				}
+			} else {
+				log.Debug("access token scope not recognized as old token scope %s; preserving it", oldScope)
+				scopes = append(scopes, oldScope)
+			}
+		}
+
+		for s := range allNewScopesMap {
+			scopes = append(scopes, string(s))
+		}
+		scope := AccessTokenScope(strings.Join(scopes, ","))
+
+		// normalize the scope
+		normScope := scope.NormalizePreservingUnknown()
+
+		token.Scope = string(normScope)
+
+		// update the db entry with the new scope
+		if _, err := x.Cols("scope").ID(token.ID).Update(token); err != nil {
+			return err
+		}
+	}
+
+	return nil
+}
diff --git a/models/migrations/v1_20/v259_test.go b/models/migrations/v1_20/v259_test.go
new file mode 100644
index 0000000000..5bc9a71391
--- /dev/null
+++ b/models/migrations/v1_20/v259_test.go
@@ -0,0 +1,110 @@
+// Copyright 2023 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package v1_20 //nolint
+
+import (
+	"sort"
+	"strings"
+	"testing"
+
+	"code.gitea.io/gitea/models/migrations/base"
+
+	"github.com/stretchr/testify/assert"
+)
+
+type testCase struct {
+	Old OldAccessTokenScope
+	New AccessTokenScope
+}
+
+func createOldTokenScope(scopes ...OldAccessTokenScope) OldAccessTokenScope {
+	s := make([]string, 0, len(scopes))
+	for _, os := range scopes {
+		s = append(s, string(os))
+	}
+	return OldAccessTokenScope(strings.Join(s, ","))
+}
+
+func createNewTokenScope(scopes ...AccessTokenScope) AccessTokenScope {
+	s := make([]string, 0, len(scopes))
+	for _, os := range scopes {
+		s = append(s, string(os))
+	}
+	return AccessTokenScope(strings.Join(s, ","))
+}
+
+func Test_ConvertScopedAccessTokens(t *testing.T) {
+	tests := []testCase{
+		{
+			createOldTokenScope(OldAccessTokenScopeRepo, OldAccessTokenScopeUserFollow),
+			createNewTokenScope(AccessTokenScopeWriteRepository, AccessTokenScopeWriteUser),
+		},
+		{
+			createOldTokenScope(OldAccessTokenScopeUser, OldAccessTokenScopeWritePackage, OldAccessTokenScopeSudo),
+			createNewTokenScope(AccessTokenScopeWriteAdmin, AccessTokenScopeWritePackage, AccessTokenScopeWriteUser),
+		},
+		{
+			createOldTokenScope(),
+			createNewTokenScope(),
+		},
+		{
+			createOldTokenScope(OldAccessTokenScopeReadGPGKey, OldAccessTokenScopeReadOrg, OldAccessTokenScopeAll),
+			createNewTokenScope(AccessTokenScopeAll),
+		},
+		{
+			createOldTokenScope(OldAccessTokenScopeReadGPGKey, "invalid"),
+			createNewTokenScope("invalid", AccessTokenScopeReadUser),
+		},
+	}
+
+	// add a test for each individual mapping
+	for oldScope, newScope := range accessTokenScopeMap {
+		tests = append(tests, testCase{
+			oldScope,
+			createNewTokenScope(newScope...),
+		})
+	}
+
+	x, deferable := base.PrepareTestEnv(t, 0, new(AccessToken))
+	defer deferable()
+	if x == nil || t.Failed() {
+		t.Skip()
+		return
+	}
+
+	// verify that no fixtures were loaded
+	count, err := x.Count(&AccessToken{})
+	assert.NoError(t, err)
+	assert.Equal(t, int64(0), count)
+
+	for _, tc := range tests {
+		_, err = x.Insert(&AccessToken{
+			Scope: string(tc.Old),
+		})
+		assert.NoError(t, err)
+	}
+
+	// migrate the scopes
+	err = ConvertScopedAccessTokens(x)
+	assert.NoError(t, err)
+
+	// migrate the scopes again (migration should be idempotent)
+	err = ConvertScopedAccessTokens(x)
+	assert.NoError(t, err)
+
+	tokens := make([]AccessToken, 0)
+	err = x.Find(&tokens)
+	assert.NoError(t, err)
+	assert.Equal(t, len(tests), len(tokens))
+
+	// sort the tokens (insertion order by auto-incrementing primary key)
+	sort.Slice(tokens, func(i, j int) bool {
+		return tokens[i].ID < tokens[j].ID
+	})
+
+	// verify that the converted scopes are equal to the expected test result
+	for idx, newToken := range tokens {
+		assert.Equal(t, string(tests[idx].New), newToken.Scope)
+	}
+}
diff --git a/modules/context/permission.go b/modules/context/permission.go
index cc53fb99ed..0f72b8e244 100644
--- a/modules/context/permission.go
+++ b/modules/context/permission.go
@@ -111,28 +111,36 @@ func RequireRepoReaderOr(unitTypes ...unit.Type) func(ctx *Context) {
 	}
 }
 
-// RequireRepoScopedToken check whether personal access token has repo scope
-func CheckRepoScopedToken(ctx *Context, repo *repo_model.Repository) {
+// CheckRepoScopedToken check whether personal access token has repo scope
+func CheckRepoScopedToken(ctx *Context, repo *repo_model.Repository, level auth_model.AccessTokenScopeLevel) {
 	if !ctx.IsBasicAuth || ctx.Data["IsApiToken"] != true {
 		return
 	}
 
-	var err error
 	scope, ok := ctx.Data["ApiTokenScope"].(auth_model.AccessTokenScope)
 	if ok { // it's a personal access token but not oauth2 token
 		var scopeMatched bool
-		scopeMatched, err = scope.HasScope(auth_model.AccessTokenScopeRepo)
+
+		requiredScopes := auth_model.GetRequiredScopes(level, auth_model.AccessTokenScopeCategoryRepository)
+
+		// check if scope only applies to public resources
+		publicOnly, err := scope.PublicOnly()
 		if err != nil {
 			ctx.ServerError("HasScope", err)
 			return
 		}
-		if !scopeMatched && !repo.IsPrivate {
-			scopeMatched, err = scope.HasScope(auth_model.AccessTokenScopePublicRepo)
-			if err != nil {
-				ctx.ServerError("HasScope", err)
-				return
-			}
+
+		if publicOnly && repo.IsPrivate {
+			ctx.Error(http.StatusForbidden)
+			return
 		}
+
+		scopeMatched, err = scope.HasScope(requiredScopes...)
+		if err != nil {
+			ctx.ServerError("HasScope", err)
+			return
+		}
+
 		if !scopeMatched {
 			ctx.Error(http.StatusForbidden)
 			return
diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini
index 517d829c84..2746e53023 100644
--- a/options/locale/locale_en-US.ini
+++ b/options/locale/locale_en-US.ini
@@ -796,7 +796,6 @@ unbind_success = The social account has been unlinked from your Gitea account.
 manage_access_token = Manage Access Tokens
 generate_new_token = Generate New Token
 tokens_desc = These tokens grant access to your account using the Gitea API.
-new_token_desc = Applications using a token have full access to your account.
 token_name = Token Name
 generate_token = Generate Token
 generate_token_success = Your new token has been generated. Copy it now as it will not be shown again.
@@ -807,8 +806,13 @@ access_token_deletion_cancel_action = Cancel
 access_token_deletion_confirm_action = Delete
 access_token_deletion_desc = Deleting a token will revoke access to your account for applications using it. This cannot be undone. Continue?
 delete_token_success = The token has been deleted. Applications using it no longer have access to your account.
-select_scopes = Select scopes
-scopes_list = Scopes:
+repo_and_org_access = Repository and Organization Access
+permissions_public_only = Public only
+permissions_access_all = All (public, private, and limited)
+select_permissions = Select permissions
+scoped_token_desc = Selected token scopes limit authentication only to the corresponding <a %s>API</a> routes. Read the <a %s>documentation</a> for more information.
+at_least_one_permission = You must select at least one permission to create a token
+permissions_list = Permissions:
 
 manage_oauth2_applications = Manage OAuth2 Applications
 edit_oauth2_application = Edit OAuth2 Application
diff --git a/routers/api/v1/api.go b/routers/api/v1/api.go
index 45e36e84fe..37361a8b96 100644
--- a/routers/api/v1/api.go
+++ b/routers/api/v1/api.go
@@ -236,44 +236,85 @@ func reqPackageAccess(accessMode perm.AccessMode) func(ctx *context.APIContext)
 	}
 }
 
+// if a token is being used for auth, we check that it contains the required scope
+// if a token is not being used, reqToken will enforce other sign in methods
+func tokenRequiresScopes(requiredScopeCategories ...auth_model.AccessTokenScopeCategory) func(ctx *context.APIContext) {
+	return func(ctx *context.APIContext) {
+		// no scope required
+		if len(requiredScopeCategories) == 0 {
+			return
+		}
+
+		// Need OAuth2 token to be present.
+		scope, scopeExists := ctx.Data["ApiTokenScope"].(auth_model.AccessTokenScope)
+		if ctx.Data["IsApiToken"] != true || !scopeExists {
+			return
+		}
+
+		ctx.Data["ApiTokenScopePublicRepoOnly"] = false
+		ctx.Data["ApiTokenScopePublicOrgOnly"] = false
+
+		// use the http method to determine the access level
+		requiredScopeLevel := auth_model.Read
+		if ctx.Req.Method == "POST" || ctx.Req.Method == "PUT" || ctx.Req.Method == "PATCH" || ctx.Req.Method == "DELETE" {
+			requiredScopeLevel = auth_model.Write
+		}
+
+		// get the required scope for the given access level and category
+		requiredScopes := auth_model.GetRequiredScopes(requiredScopeLevel, requiredScopeCategories...)
+
+		// check if scope only applies to public resources
+		publicOnly, err := scope.PublicOnly()
+		if err != nil {
+			ctx.Error(http.StatusForbidden, "tokenRequiresScope", "parsing public resource scope failed: "+err.Error())
+			return
+		}
+
+		// this context is used by the middleware in the specific route
+		ctx.Data["ApiTokenScopePublicRepoOnly"] = publicOnly && auth_model.ContainsCategory(requiredScopeCategories, auth_model.AccessTokenScopeCategoryRepository)
+		ctx.Data["ApiTokenScopePublicOrgOnly"] = publicOnly && auth_model.ContainsCategory(requiredScopeCategories, auth_model.AccessTokenScopeCategoryOrganization)
+
+		allow, err := scope.HasScope(requiredScopes...)
+		if err != nil {
+			ctx.Error(http.StatusForbidden, "tokenRequiresScope", "checking scope failed: "+err.Error())
+			return
+		}
+
+		if allow {
+			return
+		}
+
+		ctx.Error(http.StatusForbidden, "tokenRequiresScope", fmt.Sprintf("token does not have at least one of required scope(s): %v", requiredScopes))
+	}
+}
+
 // Contexter middleware already checks token for user sign in process.
-func reqToken(requiredScope auth_model.AccessTokenScope) func(ctx *context.APIContext) {
+func reqToken() func(ctx *context.APIContext) {
 	return func(ctx *context.APIContext) {
 		// If actions token is present
 		if true == ctx.Data["IsActionsToken"] {
 			return
 		}
 
-		// If OAuth2 token is present
-		if _, ok := ctx.Data["ApiTokenScope"]; ctx.Data["IsApiToken"] == true && ok {
-			// no scope required
-			if requiredScope == "" {
+		if true == ctx.Data["IsApiToken"] {
+			publicRepo, pubRepoExists := ctx.Data["ApiTokenScopePublicRepoOnly"]
+			publicOrg, pubOrgExists := ctx.Data["ApiTokenScopePublicOrgOnly"]
+
+			if pubRepoExists && publicRepo.(bool) &&
+				ctx.Repo.Repository != nil && ctx.Repo.Repository.IsPrivate {
+				ctx.Error(http.StatusForbidden, "reqToken", "token scope is limited to public repos")
 				return
 			}
 
-			// check scope
-			scope := ctx.Data["ApiTokenScope"].(auth_model.AccessTokenScope)
-			allow, err := scope.HasScope(requiredScope)
-			if err != nil {
-				ctx.Error(http.StatusForbidden, "reqToken", "parsing token failed: "+err.Error())
-				return
-			}
-			if allow {
+			if pubOrgExists && publicOrg.(bool) &&
+				ctx.Org.Organization != nil && ctx.Org.Organization.Visibility != api.VisibleTypePublic {
+				ctx.Error(http.StatusForbidden, "reqToken", "token scope is limited to public orgs")
 				return
 			}
 
-			// if requires 'repo' scope, but only has 'public_repo' scope, allow it only if the repo is public
-			if requiredScope == auth_model.AccessTokenScopeRepo {
-				if allowPublicRepo, err := scope.HasScope(auth_model.AccessTokenScopePublicRepo); err == nil && allowPublicRepo {
-					if ctx.Repo.Repository != nil && !ctx.Repo.Repository.IsPrivate {
-						return
-					}
-				}
-			}
-
-			ctx.Error(http.StatusForbidden, "reqToken", "token does not have required scope: "+requiredScope)
 			return
 		}
+
 		if ctx.IsBasicAuth {
 			ctx.CheckForOTP()
 			return
@@ -700,7 +741,7 @@ func Routes(ctx gocontext.Context) *web.Route {
 				ctx.Redirect(setting.AppSubURL + "/api/swagger")
 			})
 		}
-		m.Get("/version", misc.Version)
+
 		if setting.Federation.Enabled {
 			m.Get("/nodeinfo", misc.NodeInfo)
 			m.Group("/activitypub", func() {
@@ -713,37 +754,43 @@ func Routes(ctx gocontext.Context) *web.Route {
 					m.Get("", activitypub.Person)
 					m.Post("/inbox", activitypub.ReqHTTPSignature(), activitypub.PersonInbox)
 				}, context_service.UserIDAssignmentAPI())
-			})
+			}, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryActivityPub))
 		}
-		m.Get("/signing-key.gpg", misc.SigningKey)
-		m.Post("/markup", bind(api.MarkupOption{}), misc.Markup)
-		m.Post("/markdown", bind(api.MarkdownOption{}), misc.Markdown)
-		m.Post("/markdown/raw", misc.MarkdownRaw)
-		m.Get("/gitignore/templates", misc.ListGitignoresTemplates)
-		m.Get("/gitignore/templates/{name}", misc.GetGitignoreTemplateInfo)
-		m.Get("/licenses", misc.ListLicenseTemplates)
-		m.Get("/licenses/{name}", misc.GetLicenseTemplateInfo)
-		m.Get("/label/templates", misc.ListLabelTemplates)
-		m.Get("/label/templates/{name}", misc.GetLabelTemplate)
-		m.Group("/settings", func() {
-			m.Get("/ui", settings.GetGeneralUISettings)
-			m.Get("/api", settings.GetGeneralAPISettings)
-			m.Get("/attachment", settings.GetGeneralAttachmentSettings)
-			m.Get("/repository", settings.GetGeneralRepoSettings)
-		})
 
-		// Notifications (requires 'notification' scope)
+		// Misc (requires 'misc' scope)
+		m.Group("", func() {
+			m.Get("/version", misc.Version)
+			m.Get("/signing-key.gpg", misc.SigningKey)
+			m.Post("/markup", reqToken(), bind(api.MarkupOption{}), misc.Markup)
+			m.Post("/markdown", reqToken(), bind(api.MarkdownOption{}), misc.Markdown)
+			m.Post("/markdown/raw", reqToken(), misc.MarkdownRaw)
+			m.Get("/gitignore/templates", misc.ListGitignoresTemplates)
+			m.Get("/gitignore/templates/{name}", misc.GetGitignoreTemplateInfo)
+			m.Get("/licenses", misc.ListLicenseTemplates)
+			m.Get("/licenses/{name}", misc.GetLicenseTemplateInfo)
+			m.Get("/label/templates", misc.ListLabelTemplates)
+			m.Get("/label/templates/{name}", misc.GetLabelTemplate)
+
+			m.Group("/settings", func() {
+				m.Get("/ui", settings.GetGeneralUISettings)
+				m.Get("/api", settings.GetGeneralAPISettings)
+				m.Get("/attachment", settings.GetGeneralAttachmentSettings)
+				m.Get("/repository", settings.GetGeneralRepoSettings)
+			})
+		}, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryMisc))
+
+		// Notifications (requires 'notifications' scope)
 		m.Group("/notifications", func() {
 			m.Combo("").
 				Get(notify.ListNotifications).
-				Put(notify.ReadNotifications)
+				Put(notify.ReadNotifications, reqToken())
 			m.Get("/new", notify.NewAvailable)
 			m.Combo("/threads/{id}").
 				Get(notify.GetThread).
-				Patch(notify.ReadThread)
-		}, reqToken(auth_model.AccessTokenScopeNotification))
+				Patch(notify.ReadThread, reqToken())
+		}, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryNotification))
 
-		// Users (no scope required)
+		// Users (requires user scope)
 		m.Group("/users", func() {
 			m.Get("/search", reqExploreSignIn(), user.Search)
 
@@ -754,18 +801,18 @@ func Routes(ctx gocontext.Context) *web.Route {
 					m.Get("/heatmap", user.GetUserHeatmapData)
 				}
 
-				m.Get("/repos", reqExploreSignIn(), user.ListUserRepos)
+				m.Get("/repos", tokenRequiresScopes(auth_model.AccessTokenScopeCategoryRepository), reqExploreSignIn(), user.ListUserRepos)
 				m.Group("/tokens", func() {
 					m.Combo("").Get(user.ListAccessTokens).
-						Post(bind(api.CreateAccessTokenOption{}), user.CreateAccessToken)
-					m.Combo("/{id}").Delete(user.DeleteAccessToken)
+						Post(bind(api.CreateAccessTokenOption{}), reqToken(), user.CreateAccessToken)
+					m.Combo("/{id}").Delete(reqToken(), user.DeleteAccessToken)
 				}, reqBasicAuth())
 
 				m.Get("/activities/feeds", user.ListUserActivityFeeds)
 			}, context_service.UserAssignmentAPI())
-		})
+		}, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryUser))
 
-		// (no scope required)
+		// Users (requires user scope)
 		m.Group("/users", func() {
 			m.Group("/{username}", func() {
 				m.Get("/keys", user.ListPublicKeys)
@@ -781,59 +828,61 @@ func Routes(ctx gocontext.Context) *web.Route {
 
 				m.Get("/subscriptions", user.GetWatchedRepos)
 			}, context_service.UserAssignmentAPI())
-		}, reqToken(""))
+		}, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryUser), reqToken())
 
+		// Users (requires user scope)
 		m.Group("/user", func() {
 			m.Get("", user.GetAuthenticatedUser)
 			m.Group("/settings", func() {
-				m.Get("", reqToken(auth_model.AccessTokenScopeReadUser), user.GetUserSettings)
-				m.Patch("", reqToken(auth_model.AccessTokenScopeUser), bind(api.UserSettingsOptions{}), user.UpdateUserSettings)
-			})
-			m.Combo("/emails").Get(reqToken(auth_model.AccessTokenScopeReadUser), user.ListEmails).
-				Post(reqToken(auth_model.AccessTokenScopeUser), bind(api.CreateEmailOption{}), user.AddEmail).
-				Delete(reqToken(auth_model.AccessTokenScopeUser), bind(api.DeleteEmailOption{}), user.DeleteEmail)
+				m.Get("", user.GetUserSettings)
+				m.Patch("", bind(api.UserSettingsOptions{}), user.UpdateUserSettings)
+			}, reqToken())
+			m.Combo("/emails").
+				Get(user.ListEmails).
+				Post(bind(api.CreateEmailOption{}), user.AddEmail).
+				Delete(bind(api.DeleteEmailOption{}), user.DeleteEmail)
 
 			m.Get("/followers", user.ListMyFollowers)
 			m.Group("/following", func() {
 				m.Get("", user.ListMyFollowing)
 				m.Group("/{username}", func() {
 					m.Get("", user.CheckMyFollowing)
-					m.Put("", reqToken(auth_model.AccessTokenScopeUserFollow), user.Follow)      // requires 'user:follow' scope
-					m.Delete("", reqToken(auth_model.AccessTokenScopeUserFollow), user.Unfollow) // requires 'user:follow' scope
+					m.Put("", user.Follow)
+					m.Delete("", user.Unfollow)
 				}, context_service.UserAssignmentAPI())
 			})
 
 			// (admin:public_key scope)
 			m.Group("/keys", func() {
-				m.Combo("").Get(reqToken(auth_model.AccessTokenScopeReadPublicKey), user.ListMyPublicKeys).
-					Post(reqToken(auth_model.AccessTokenScopeWritePublicKey), bind(api.CreateKeyOption{}), user.CreatePublicKey)
-				m.Combo("/{id}").Get(reqToken(auth_model.AccessTokenScopeReadPublicKey), user.GetPublicKey).
-					Delete(reqToken(auth_model.AccessTokenScopeWritePublicKey), user.DeletePublicKey)
+				m.Combo("").Get(user.ListMyPublicKeys).
+					Post(bind(api.CreateKeyOption{}), user.CreatePublicKey)
+				m.Combo("/{id}").Get(user.GetPublicKey).
+					Delete(user.DeletePublicKey)
 			})
 
 			// (admin:application scope)
 			m.Group("/applications", func() {
 				m.Combo("/oauth2").
-					Get(reqToken(auth_model.AccessTokenScopeReadApplication), user.ListOauth2Applications).
-					Post(reqToken(auth_model.AccessTokenScopeWriteApplication), bind(api.CreateOAuth2ApplicationOptions{}), user.CreateOauth2Application)
+					Get(user.ListOauth2Applications).
+					Post(bind(api.CreateOAuth2ApplicationOptions{}), user.CreateOauth2Application)
 				m.Combo("/oauth2/{id}").
-					Delete(reqToken(auth_model.AccessTokenScopeWriteApplication), user.DeleteOauth2Application).
-					Patch(reqToken(auth_model.AccessTokenScopeWriteApplication), bind(api.CreateOAuth2ApplicationOptions{}), user.UpdateOauth2Application).
-					Get(reqToken(auth_model.AccessTokenScopeReadApplication), user.GetOauth2Application)
+					Delete(user.DeleteOauth2Application).
+					Patch(bind(api.CreateOAuth2ApplicationOptions{}), user.UpdateOauth2Application).
+					Get(user.GetOauth2Application)
 			})
 
 			// (admin:gpg_key scope)
 			m.Group("/gpg_keys", func() {
-				m.Combo("").Get(reqToken(auth_model.AccessTokenScopeReadGPGKey), user.ListMyGPGKeys).
-					Post(reqToken(auth_model.AccessTokenScopeWriteGPGKey), bind(api.CreateGPGKeyOption{}), user.CreateGPGKey)
-				m.Combo("/{id}").Get(reqToken(auth_model.AccessTokenScopeReadGPGKey), user.GetGPGKey).
-					Delete(reqToken(auth_model.AccessTokenScopeWriteGPGKey), user.DeleteGPGKey)
+				m.Combo("").Get(user.ListMyGPGKeys).
+					Post(bind(api.CreateGPGKeyOption{}), user.CreateGPGKey)
+				m.Combo("/{id}").Get(user.GetGPGKey).
+					Delete(user.DeleteGPGKey)
 			})
-			m.Get("/gpg_key_token", reqToken(auth_model.AccessTokenScopeReadGPGKey), user.GetVerificationToken)
-			m.Post("/gpg_key_verify", reqToken(auth_model.AccessTokenScopeReadGPGKey), bind(api.VerifyGPGKeyOption{}), user.VerifyUserGPGKey)
+			m.Get("/gpg_key_token", user.GetVerificationToken)
+			m.Post("/gpg_key_verify", bind(api.VerifyGPGKeyOption{}), user.VerifyUserGPGKey)
 
 			// (repo scope)
-			m.Combo("/repos", reqToken(auth_model.AccessTokenScopeRepo)).Get(user.ListMyRepos).
+			m.Combo("/repos", tokenRequiresScopes(auth_model.AccessTokenScopeCategoryRepository)).Get(user.ListMyRepos).
 				Post(bind(api.CreateRepoOption{}), repo.Create)
 
 			// (repo scope)
@@ -844,64 +893,65 @@ func Routes(ctx gocontext.Context) *web.Route {
 					m.Put("", user.Star)
 					m.Delete("", user.Unstar)
 				}, repoAssignment())
-			}, reqToken(auth_model.AccessTokenScopeRepo))
-			m.Get("/times", reqToken(auth_model.AccessTokenScopeRepo), repo.ListMyTrackedTimes)
-			m.Get("/stopwatches", reqToken(auth_model.AccessTokenScopeRepo), repo.GetStopwatches)
-			m.Get("/subscriptions", reqToken(auth_model.AccessTokenScopeRepo), user.GetMyWatchedRepos)
-			m.Get("/teams", reqToken(auth_model.AccessTokenScopeRepo), org.ListUserTeams)
+			}, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryRepository))
+			m.Get("/times", repo.ListMyTrackedTimes)
+			m.Get("/stopwatches", repo.GetStopwatches)
+			m.Get("/subscriptions", user.GetMyWatchedRepos)
+			m.Get("/teams", org.ListUserTeams)
 			m.Group("/hooks", func() {
 				m.Combo("").Get(user.ListHooks).
 					Post(bind(api.CreateHookOption{}), user.CreateHook)
 				m.Combo("/{id}").Get(user.GetHook).
 					Patch(bind(api.EditHookOption{}), user.EditHook).
 					Delete(user.DeleteHook)
-			}, reqToken(auth_model.AccessTokenScopeAdminUserHook), reqWebhooksEnabled())
-		}, reqToken(""))
+			}, reqWebhooksEnabled())
+		}, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryUser), reqToken())
 
-		// Repositories
-		m.Post("/org/{org}/repos", reqToken(auth_model.AccessTokenScopeAdminOrg), bind(api.CreateRepoOption{}), repo.CreateOrgRepoDeprecated)
+		// Repositories (requires repo scope, org scope)
+		m.Post("/org/{org}/repos",
+			tokenRequiresScopes(auth_model.AccessTokenScopeCategoryOrganization, auth_model.AccessTokenScopeCategoryRepository),
+			reqToken(),
+			bind(api.CreateRepoOption{}),
+			repo.CreateOrgRepoDeprecated)
 
-		m.Combo("/repositories/{id}", reqToken(auth_model.AccessTokenScopeRepo)).Get(repo.GetByID)
+		// requires repo scope
+		m.Combo("/repositories/{id}", reqToken(), tokenRequiresScopes(auth_model.AccessTokenScopeCategoryRepository)).Get(repo.GetByID)
 
+		// Repos (requires repo scope)
 		m.Group("/repos", func() {
 			m.Get("/search", repo.Search)
 
-			m.Get("/issues/search", repo.SearchIssues)
-
 			// (repo scope)
-			m.Post("/migrate", reqToken(auth_model.AccessTokenScopeRepo), bind(api.MigrateRepoOptions{}), repo.Migrate)
+			m.Post("/migrate", reqToken(), bind(api.MigrateRepoOptions{}), repo.Migrate)
 
 			m.Group("/{username}/{reponame}", func() {
 				m.Combo("").Get(reqAnyRepoReader(), repo.Get).
-					Delete(reqToken(auth_model.AccessTokenScopeDeleteRepo), reqOwner(), repo.Delete).
-					Patch(reqToken(auth_model.AccessTokenScopeRepo), reqAdmin(), bind(api.EditRepoOption{}), repo.Edit)
-				m.Post("/generate", reqToken(auth_model.AccessTokenScopeRepo), reqRepoReader(unit.TypeCode), bind(api.GenerateRepoOption{}), repo.Generate)
+					Delete(reqToken(), reqOwner(), repo.Delete).
+					Patch(reqToken(), reqAdmin(), bind(api.EditRepoOption{}), repo.Edit)
+				m.Post("/generate", reqToken(), reqRepoReader(unit.TypeCode), bind(api.GenerateRepoOption{}), repo.Generate)
 				m.Group("/transfer", func() {
 					m.Post("", reqOwner(), bind(api.TransferRepoOption{}), repo.Transfer)
 					m.Post("/accept", repo.AcceptTransfer)
 					m.Post("/reject", repo.RejectTransfer)
-				}, reqToken(auth_model.AccessTokenScopeRepo))
-				m.Combo("/notifications", reqToken(auth_model.AccessTokenScopeNotification)).
-					Get(notify.ListRepoNotifications).
-					Put(notify.ReadRepoNotifications)
+				}, reqToken())
 				m.Group("/hooks/git", func() {
-					m.Combo("").Get(reqToken(auth_model.AccessTokenScopeReadRepoHook), repo.ListGitHooks)
+					m.Combo("").Get(repo.ListGitHooks)
 					m.Group("/{id}", func() {
-						m.Combo("").Get(reqToken(auth_model.AccessTokenScopeReadRepoHook), repo.GetGitHook).
-							Patch(reqToken(auth_model.AccessTokenScopeWriteRepoHook), bind(api.EditGitHookOption{}), repo.EditGitHook).
-							Delete(reqToken(auth_model.AccessTokenScopeWriteRepoHook), repo.DeleteGitHook)
+						m.Combo("").Get(repo.GetGitHook).
+							Patch(bind(api.EditGitHookOption{}), repo.EditGitHook).
+							Delete(repo.DeleteGitHook)
 					})
-				}, reqAdmin(), reqGitHook(), context.ReferencesGitRepo(true))
+				}, reqToken(), reqAdmin(), reqGitHook(), context.ReferencesGitRepo(true))
 				m.Group("/hooks", func() {
-					m.Combo("").Get(reqToken(auth_model.AccessTokenScopeReadRepoHook), repo.ListHooks).
-						Post(reqToken(auth_model.AccessTokenScopeWriteRepoHook), bind(api.CreateHookOption{}), repo.CreateHook)
+					m.Combo("").Get(repo.ListHooks).
+						Post(bind(api.CreateHookOption{}), repo.CreateHook)
 					m.Group("/{id}", func() {
-						m.Combo("").Get(reqToken(auth_model.AccessTokenScopeReadRepoHook), repo.GetHook).
-							Patch(reqToken(auth_model.AccessTokenScopeWriteRepoHook), bind(api.EditHookOption{}), repo.EditHook).
-							Delete(reqToken(auth_model.AccessTokenScopeWriteRepoHook), repo.DeleteHook)
-						m.Post("/tests", reqToken(auth_model.AccessTokenScopeReadRepoHook), context.ReferencesGitRepo(), context.RepoRefForAPI, repo.TestHook)
+						m.Combo("").Get(repo.GetHook).
+							Patch(bind(api.EditHookOption{}), repo.EditHook).
+							Delete(repo.DeleteHook)
+						m.Post("/tests", context.ReferencesGitRepo(), context.RepoRefForAPI, repo.TestHook)
 					})
-				}, reqAdmin(), reqWebhooksEnabled())
+				}, reqToken(), reqAdmin(), reqWebhooksEnabled())
 				m.Group("/collaborators", func() {
 					m.Get("", reqAnyRepoReader(), repo.ListCollaborators)
 					m.Group("/{collaborator}", func() {
@@ -910,25 +960,25 @@ func Routes(ctx gocontext.Context) *web.Route {
 							Delete(reqAdmin(), repo.DeleteCollaborator)
 						m.Get("/permission", repo.GetRepoPermissions)
 					})
-				}, reqToken(auth_model.AccessTokenScopeRepo))
-				m.Get("/assignees", reqToken(auth_model.AccessTokenScopeRepo), reqAnyRepoReader(), repo.GetAssignees)
-				m.Get("/reviewers", reqToken(auth_model.AccessTokenScopeRepo), reqAnyRepoReader(), repo.GetReviewers)
+				}, reqToken())
+				m.Get("/assignees", reqToken(), reqAnyRepoReader(), repo.GetAssignees)
+				m.Get("/reviewers", reqToken(), reqAnyRepoReader(), repo.GetReviewers)
 				m.Group("/teams", func() {
 					m.Get("", reqAnyRepoReader(), repo.ListTeams)
 					m.Combo("/{team}").Get(reqAnyRepoReader(), repo.IsTeam).
 						Put(reqAdmin(), repo.AddTeam).
 						Delete(reqAdmin(), repo.DeleteTeam)
-				}, reqToken(auth_model.AccessTokenScopeRepo))
+				}, reqToken())
 				m.Get("/raw/*", context.ReferencesGitRepo(), context.RepoRefForAPI, reqRepoReader(unit.TypeCode), repo.GetRawFile)
 				m.Get("/media/*", context.ReferencesGitRepo(), context.RepoRefForAPI, reqRepoReader(unit.TypeCode), repo.GetRawFileOrLFS)
 				m.Get("/archive/*", reqRepoReader(unit.TypeCode), repo.GetArchive)
 				m.Combo("/forks").Get(repo.ListForks).
-					Post(reqToken(auth_model.AccessTokenScopeRepo), reqRepoReader(unit.TypeCode), bind(api.CreateForkOption{}), repo.CreateFork)
+					Post(reqToken(), reqRepoReader(unit.TypeCode), bind(api.CreateForkOption{}), repo.CreateFork)
 				m.Group("/branches", func() {
 					m.Get("", repo.ListBranches)
 					m.Get("/*", repo.GetBranch)
-					m.Delete("/*", reqToken(auth_model.AccessTokenScopeRepo), reqRepoWriter(unit.TypeCode), repo.DeleteBranch)
-					m.Post("", reqToken(auth_model.AccessTokenScopeRepo), reqRepoWriter(unit.TypeCode), bind(api.CreateBranchRepoOption{}), repo.CreateBranch)
+					m.Delete("/*", reqToken(), reqRepoWriter(unit.TypeCode), repo.DeleteBranch)
+					m.Post("", reqToken(), reqRepoWriter(unit.TypeCode), bind(api.CreateBranchRepoOption{}), repo.CreateBranch)
 				}, context.ReferencesGitRepo(), reqRepoReader(unit.TypeCode))
 				m.Group("/branch_protections", func() {
 					m.Get("", repo.ListBranchProtections)
@@ -938,218 +988,112 @@ func Routes(ctx gocontext.Context) *web.Route {
 						m.Patch("", bind(api.EditBranchProtectionOption{}), repo.EditBranchProtection)
 						m.Delete("", repo.DeleteBranchProtection)
 					})
-				}, reqToken(auth_model.AccessTokenScopeRepo), reqAdmin())
+				}, reqToken(), reqAdmin())
 				m.Group("/tags", func() {
 					m.Get("", repo.ListTags)
 					m.Get("/*", repo.GetTag)
-					m.Post("", reqToken(auth_model.AccessTokenScopeRepo), reqRepoWriter(unit.TypeCode), bind(api.CreateTagOption{}), repo.CreateTag)
-					m.Delete("/*", reqToken(auth_model.AccessTokenScopeRepo), repo.DeleteTag)
+					m.Post("", reqToken(), reqRepoWriter(unit.TypeCode), bind(api.CreateTagOption{}), repo.CreateTag)
+					m.Delete("/*", reqToken(), repo.DeleteTag)
 				}, reqRepoReader(unit.TypeCode), context.ReferencesGitRepo(true))
 				m.Group("/keys", func() {
 					m.Combo("").Get(repo.ListDeployKeys).
 						Post(bind(api.CreateKeyOption{}), repo.CreateDeployKey)
 					m.Combo("/{id}").Get(repo.GetDeployKey).
 						Delete(repo.DeleteDeploykey)
-				}, reqToken(auth_model.AccessTokenScopeRepo), reqAdmin())
+				}, reqToken(), reqAdmin())
 				m.Group("/times", func() {
 					m.Combo("").Get(repo.ListTrackedTimesByRepository)
 					m.Combo("/{timetrackingusername}").Get(repo.ListTrackedTimesByUser)
-				}, mustEnableIssues, reqToken(auth_model.AccessTokenScopeRepo))
+				}, mustEnableIssues, reqToken())
 				m.Group("/wiki", func() {
 					m.Combo("/page/{pageName}").
 						Get(repo.GetWikiPage).
-						Patch(mustNotBeArchived, reqToken(auth_model.AccessTokenScopeRepo), reqRepoWriter(unit.TypeWiki), bind(api.CreateWikiPageOptions{}), repo.EditWikiPage).
-						Delete(mustNotBeArchived, reqToken(auth_model.AccessTokenScopeRepo), reqRepoWriter(unit.TypeWiki), repo.DeleteWikiPage)
+						Patch(mustNotBeArchived, reqToken(), reqRepoWriter(unit.TypeWiki), bind(api.CreateWikiPageOptions{}), repo.EditWikiPage).
+						Delete(mustNotBeArchived, reqToken(), reqRepoWriter(unit.TypeWiki), repo.DeleteWikiPage)
 					m.Get("/revisions/{pageName}", repo.ListPageRevisions)
-					m.Post("/new", mustNotBeArchived, reqToken(auth_model.AccessTokenScopeRepo), reqRepoWriter(unit.TypeWiki), bind(api.CreateWikiPageOptions{}), repo.NewWikiPage)
+					m.Post("/new", reqToken(), mustNotBeArchived, reqRepoWriter(unit.TypeWiki), bind(api.CreateWikiPageOptions{}), repo.NewWikiPage)
 					m.Get("/pages", repo.ListWikiPages)
 				}, mustEnableWiki)
-				m.Group("/issues", func() {
-					m.Combo("").Get(repo.ListIssues).
-						Post(reqToken(auth_model.AccessTokenScopeRepo), mustNotBeArchived, bind(api.CreateIssueOption{}), repo.CreateIssue)
-					m.Get("/pinned", repo.ListPinnedIssues)
-					m.Group("/comments", func() {
-						m.Get("", repo.ListRepoIssueComments)
-						m.Group("/{id}", func() {
-							m.Combo("").
-								Get(repo.GetIssueComment).
-								Patch(mustNotBeArchived, reqToken(auth_model.AccessTokenScopeRepo), bind(api.EditIssueCommentOption{}), repo.EditIssueComment).
-								Delete(reqToken(auth_model.AccessTokenScopeRepo), repo.DeleteIssueComment)
-							m.Combo("/reactions").
-								Get(repo.GetIssueCommentReactions).
-								Post(reqToken(auth_model.AccessTokenScopeRepo), bind(api.EditReactionOption{}), repo.PostIssueCommentReaction).
-								Delete(reqToken(auth_model.AccessTokenScopeRepo), bind(api.EditReactionOption{}), repo.DeleteIssueCommentReaction)
-							m.Group("/assets", func() {
-								m.Combo("").
-									Get(repo.ListIssueCommentAttachments).
-									Post(reqToken(auth_model.AccessTokenScopeRepo), mustNotBeArchived, repo.CreateIssueCommentAttachment)
-								m.Combo("/{asset}").
-									Get(repo.GetIssueCommentAttachment).
-									Patch(reqToken(auth_model.AccessTokenScopeRepo), mustNotBeArchived, bind(api.EditAttachmentOptions{}), repo.EditIssueCommentAttachment).
-									Delete(reqToken(auth_model.AccessTokenScopeRepo), mustNotBeArchived, repo.DeleteIssueCommentAttachment)
-							}, mustEnableAttachments)
-						})
-					})
-					m.Group("/{index}", func() {
-						m.Combo("").Get(repo.GetIssue).
-							Patch(reqToken(auth_model.AccessTokenScopeRepo), bind(api.EditIssueOption{}), repo.EditIssue).
-							Delete(reqToken(auth_model.AccessTokenScopeRepo), reqAdmin(), context.ReferencesGitRepo(), repo.DeleteIssue)
-						m.Group("/comments", func() {
-							m.Combo("").Get(repo.ListIssueComments).
-								Post(reqToken(auth_model.AccessTokenScopeRepo), mustNotBeArchived, bind(api.CreateIssueCommentOption{}), repo.CreateIssueComment)
-							m.Combo("/{id}", reqToken(auth_model.AccessTokenScopeRepo)).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(auth_model.AccessTokenScopeRepo), bind(api.IssueLabelsOption{}), repo.AddIssueLabels).
-								Put(reqToken(auth_model.AccessTokenScopeRepo), bind(api.IssueLabelsOption{}), repo.ReplaceIssueLabels).
-								Delete(reqToken(auth_model.AccessTokenScopeRepo), repo.ClearIssueLabels)
-							m.Delete("/{id}", reqToken(auth_model.AccessTokenScopeRepo), 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)
-						}, reqToken(auth_model.AccessTokenScopeRepo))
-						m.Combo("/deadline").Post(reqToken(auth_model.AccessTokenScopeRepo), bind(api.EditDeadlineOption{}), repo.UpdateIssueDeadline)
-						m.Group("/stopwatch", func() {
-							m.Post("/start", reqToken(auth_model.AccessTokenScopeRepo), repo.StartIssueStopwatch)
-							m.Post("/stop", reqToken(auth_model.AccessTokenScopeRepo), repo.StopIssueStopwatch)
-							m.Delete("/delete", reqToken(auth_model.AccessTokenScopeRepo), repo.DeleteIssueStopwatch)
-						})
-						m.Group("/subscriptions", func() {
-							m.Get("", repo.GetIssueSubscribers)
-							m.Get("/check", reqToken(auth_model.AccessTokenScopeRepo), repo.CheckIssueSubscription)
-							m.Put("/{user}", reqToken(auth_model.AccessTokenScopeRepo), repo.AddIssueSubscription)
-							m.Delete("/{user}", reqToken(auth_model.AccessTokenScopeRepo), repo.DelIssueSubscription)
-						})
-						m.Combo("/reactions").
-							Get(repo.GetIssueReactions).
-							Post(reqToken(auth_model.AccessTokenScopeRepo), bind(api.EditReactionOption{}), repo.PostIssueReaction).
-							Delete(reqToken(auth_model.AccessTokenScopeRepo), bind(api.EditReactionOption{}), repo.DeleteIssueReaction)
-						m.Group("/assets", func() {
-							m.Combo("").
-								Get(repo.ListIssueAttachments).
-								Post(reqToken(auth_model.AccessTokenScopeRepo), mustNotBeArchived, repo.CreateIssueAttachment)
-							m.Combo("/{asset}").
-								Get(repo.GetIssueAttachment).
-								Patch(reqToken(auth_model.AccessTokenScopeRepo), mustNotBeArchived, bind(api.EditAttachmentOptions{}), repo.EditIssueAttachment).
-								Delete(reqToken(auth_model.AccessTokenScopeRepo), mustNotBeArchived, repo.DeleteIssueAttachment)
-						}, mustEnableAttachments)
-						m.Combo("/dependencies").
-							Get(repo.GetIssueDependencies).
-							Post(reqToken(auth_model.AccessTokenScopeRepo), mustNotBeArchived, bind(api.IssueMeta{}), repo.CreateIssueDependency).
-							Delete(reqToken(auth_model.AccessTokenScopeRepo), mustNotBeArchived, bind(api.IssueMeta{}), repo.RemoveIssueDependency)
-						m.Combo("/blocks").
-							Get(repo.GetIssueBlocks).
-							Post(reqToken(auth_model.AccessTokenScopeRepo), bind(api.IssueMeta{}), repo.CreateIssueBlocking).
-							Delete(reqToken(auth_model.AccessTokenScopeRepo), bind(api.IssueMeta{}), repo.RemoveIssueBlocking)
-						m.Group("/pin", func() {
-							m.Combo("").
-								Post(reqToken(auth_model.AccessTokenScopeRepo), reqAdmin(), repo.PinIssue).
-								Delete(reqToken(auth_model.AccessTokenScopeRepo), reqAdmin(), repo.UnpinIssue)
-							m.Patch("/{position}", reqToken(auth_model.AccessTokenScopeRepo), reqAdmin(), repo.MoveIssuePin)
-						})
-					})
-				}, mustEnableIssuesOrPulls)
-				m.Group("/labels", func() {
-					m.Combo("").Get(repo.ListLabels).
-						Post(reqToken(auth_model.AccessTokenScopeRepo), reqRepoWriter(unit.TypeIssues, unit.TypePullRequests), bind(api.CreateLabelOption{}), repo.CreateLabel)
-					m.Combo("/{id}").Get(repo.GetLabel).
-						Patch(reqToken(auth_model.AccessTokenScopeRepo), reqRepoWriter(unit.TypeIssues, unit.TypePullRequests), bind(api.EditLabelOption{}), repo.EditLabel).
-						Delete(reqToken(auth_model.AccessTokenScopeRepo), reqRepoWriter(unit.TypeIssues, unit.TypePullRequests), repo.DeleteLabel)
-				})
-				m.Post("/markup", reqToken(auth_model.AccessTokenScopeRepo), bind(api.MarkupOption{}), misc.Markup)
-				m.Post("/markdown", reqToken(auth_model.AccessTokenScopeRepo), bind(api.MarkdownOption{}), misc.Markdown)
-				m.Post("/markdown/raw", reqToken(auth_model.AccessTokenScopeRepo), misc.MarkdownRaw)
-				m.Group("/milestones", func() {
-					m.Combo("").Get(repo.ListMilestones).
-						Post(reqToken(auth_model.AccessTokenScopeRepo), reqRepoWriter(unit.TypeIssues, unit.TypePullRequests), bind(api.CreateMilestoneOption{}), repo.CreateMilestone)
-					m.Combo("/{id}").Get(repo.GetMilestone).
-						Patch(reqToken(auth_model.AccessTokenScopeRepo), reqRepoWriter(unit.TypeIssues, unit.TypePullRequests), bind(api.EditMilestoneOption{}), repo.EditMilestone).
-						Delete(reqToken(auth_model.AccessTokenScopeRepo), reqRepoWriter(unit.TypeIssues, unit.TypePullRequests), repo.DeleteMilestone)
-				})
+				m.Post("/markup", reqToken(), bind(api.MarkupOption{}), misc.Markup)
+				m.Post("/markdown", reqToken(), bind(api.MarkdownOption{}), misc.Markdown)
+				m.Post("/markdown/raw", reqToken(), misc.MarkdownRaw)
 				m.Get("/stargazers", repo.ListStargazers)
 				m.Get("/subscribers", repo.ListSubscribers)
 				m.Group("/subscription", func() {
 					m.Get("", user.IsWatching)
-					m.Put("", reqToken(auth_model.AccessTokenScopeRepo), user.Watch)
-					m.Delete("", reqToken(auth_model.AccessTokenScopeRepo), user.Unwatch)
+					m.Put("", reqToken(), user.Watch)
+					m.Delete("", reqToken(), user.Unwatch)
 				})
 				m.Group("/releases", func() {
 					m.Combo("").Get(repo.ListReleases).
-						Post(reqToken(auth_model.AccessTokenScopeRepo), reqRepoWriter(unit.TypeReleases), context.ReferencesGitRepo(), bind(api.CreateReleaseOption{}), repo.CreateRelease)
+						Post(reqToken(), reqRepoWriter(unit.TypeReleases), context.ReferencesGitRepo(), bind(api.CreateReleaseOption{}), repo.CreateRelease)
 					m.Combo("/latest").Get(repo.GetLatestRelease)
 					m.Group("/{id}", func() {
 						m.Combo("").Get(repo.GetRelease).
-							Patch(reqToken(auth_model.AccessTokenScopeRepo), reqRepoWriter(unit.TypeReleases), context.ReferencesGitRepo(), bind(api.EditReleaseOption{}), repo.EditRelease).
-							Delete(reqToken(auth_model.AccessTokenScopeRepo), reqRepoWriter(unit.TypeReleases), repo.DeleteRelease)
+							Patch(reqToken(), reqRepoWriter(unit.TypeReleases), context.ReferencesGitRepo(), bind(api.EditReleaseOption{}), repo.EditRelease).
+							Delete(reqToken(), reqRepoWriter(unit.TypeReleases), repo.DeleteRelease)
 						m.Group("/assets", func() {
 							m.Combo("").Get(repo.ListReleaseAttachments).
-								Post(reqToken(auth_model.AccessTokenScopeRepo), reqRepoWriter(unit.TypeReleases), repo.CreateReleaseAttachment)
+								Post(reqToken(), reqRepoWriter(unit.TypeReleases), repo.CreateReleaseAttachment)
 							m.Combo("/{asset}").Get(repo.GetReleaseAttachment).
-								Patch(reqToken(auth_model.AccessTokenScopeRepo), reqRepoWriter(unit.TypeReleases), bind(api.EditAttachmentOptions{}), repo.EditReleaseAttachment).
-								Delete(reqToken(auth_model.AccessTokenScopeRepo), reqRepoWriter(unit.TypeReleases), repo.DeleteReleaseAttachment)
+								Patch(reqToken(), reqRepoWriter(unit.TypeReleases), bind(api.EditAttachmentOptions{}), repo.EditReleaseAttachment).
+								Delete(reqToken(), reqRepoWriter(unit.TypeReleases), repo.DeleteReleaseAttachment)
 						})
 					})
 					m.Group("/tags", func() {
 						m.Combo("/{tag}").
 							Get(repo.GetReleaseByTag).
-							Delete(reqToken(auth_model.AccessTokenScopeRepo), reqRepoWriter(unit.TypeReleases), repo.DeleteReleaseByTag)
+							Delete(reqToken(), reqRepoWriter(unit.TypeReleases), repo.DeleteReleaseByTag)
 					})
 				}, reqRepoReader(unit.TypeReleases))
-				m.Post("/mirror-sync", reqToken(auth_model.AccessTokenScopeRepo), reqRepoWriter(unit.TypeCode), repo.MirrorSync)
-				m.Post("/push_mirrors-sync", reqAdmin(), reqToken(auth_model.AccessTokenScopeRepo), repo.PushMirrorSync)
+				m.Post("/mirror-sync", reqToken(), reqRepoWriter(unit.TypeCode), repo.MirrorSync)
+				m.Post("/push_mirrors-sync", reqAdmin(), reqToken(), repo.PushMirrorSync)
 				m.Group("/push_mirrors", func() {
 					m.Combo("").Get(repo.ListPushMirrors).
 						Post(bind(api.CreatePushMirrorOption{}), repo.AddPushMirror)
 					m.Combo("/{name}").
 						Delete(repo.DeletePushMirrorByRemoteName).
 						Get(repo.GetPushMirrorByName)
-				}, reqAdmin(), reqToken(auth_model.AccessTokenScopeRepo))
+				}, reqAdmin(), reqToken())
 
 				m.Get("/editorconfig/{filename}", context.ReferencesGitRepo(), context.RepoRefForAPI, reqRepoReader(unit.TypeCode), repo.GetEditorconfig)
 				m.Group("/pulls", func() {
 					m.Combo("").Get(repo.ListPullRequests).
-						Post(reqToken(auth_model.AccessTokenScopeRepo), mustNotBeArchived, bind(api.CreatePullRequestOption{}), repo.CreatePullRequest)
+						Post(reqToken(), mustNotBeArchived, bind(api.CreatePullRequestOption{}), repo.CreatePullRequest)
 					m.Get("/pinned", repo.ListPinnedPullRequests)
 					m.Group("/{index}", func() {
 						m.Combo("").Get(repo.GetPullRequest).
-							Patch(reqToken(auth_model.AccessTokenScopeRepo), bind(api.EditPullRequestOption{}), repo.EditPullRequest)
+							Patch(reqToken(), bind(api.EditPullRequestOption{}), repo.EditPullRequest)
 						m.Get(".{diffType:diff|patch}", repo.DownloadPullDiffOrPatch)
-						m.Post("/update", reqToken(auth_model.AccessTokenScopeRepo), repo.UpdatePullRequest)
+						m.Post("/update", reqToken(), repo.UpdatePullRequest)
 						m.Get("/commits", repo.GetPullRequestCommits)
 						m.Get("/files", repo.GetPullRequestFiles)
 						m.Combo("/merge").Get(repo.IsPullRequestMerged).
-							Post(reqToken(auth_model.AccessTokenScopeRepo), mustNotBeArchived, bind(forms.MergePullRequestForm{}), repo.MergePullRequest).
-							Delete(reqToken(auth_model.AccessTokenScopeRepo), mustNotBeArchived, repo.CancelScheduledAutoMerge)
+							Post(reqToken(), mustNotBeArchived, bind(forms.MergePullRequestForm{}), repo.MergePullRequest).
+							Delete(reqToken(), mustNotBeArchived, repo.CancelScheduledAutoMerge)
 						m.Group("/reviews", func() {
 							m.Combo("").
 								Get(repo.ListPullReviews).
-								Post(reqToken(auth_model.AccessTokenScopeRepo), bind(api.CreatePullReviewOptions{}), repo.CreatePullReview)
+								Post(reqToken(), bind(api.CreatePullReviewOptions{}), repo.CreatePullReview)
 							m.Group("/{id}", func() {
 								m.Combo("").
 									Get(repo.GetPullReview).
-									Delete(reqToken(auth_model.AccessTokenScopeRepo), repo.DeletePullReview).
-									Post(reqToken(auth_model.AccessTokenScopeRepo), bind(api.SubmitPullReviewOptions{}), repo.SubmitPullReview)
+									Delete(reqToken(), repo.DeletePullReview).
+									Post(reqToken(), bind(api.SubmitPullReviewOptions{}), repo.SubmitPullReview)
 								m.Combo("/comments").
 									Get(repo.GetPullReviewComments)
-								m.Post("/dismissals", reqToken(auth_model.AccessTokenScopeRepo), bind(api.DismissPullReviewOptions{}), repo.DismissPullReview)
-								m.Post("/undismissals", reqToken(auth_model.AccessTokenScopeRepo), repo.UnDismissPullReview)
+								m.Post("/dismissals", reqToken(), bind(api.DismissPullReviewOptions{}), repo.DismissPullReview)
+								m.Post("/undismissals", reqToken(), repo.UnDismissPullReview)
 							})
 						})
-						m.Combo("/requested_reviewers", reqToken(auth_model.AccessTokenScopeRepo)).
+						m.Combo("/requested_reviewers", reqToken()).
 							Delete(bind(api.PullReviewRequestOptions{}), repo.DeleteReviewRequests).
 							Post(bind(api.PullReviewRequestOptions{}), repo.CreateReviewRequests)
 					})
 				}, mustAllowPulls, reqRepoReader(unit.TypeCode), context.ReferencesGitRepo())
 				m.Group("/statuses", func() {
 					m.Combo("/{sha}").Get(repo.GetCommitStatuses).
-						Post(reqToken(auth_model.AccessTokenScopeRepoStatus), reqRepoWriter(unit.TypeCode), bind(api.CreateStatusOption{}), repo.NewCommitStatus)
+						Post(reqToken(), reqRepoWriter(unit.TypeCode), bind(api.CreateStatusOption{}), repo.NewCommitStatus)
 				}, reqRepoReader(unit.TypeCode))
 				m.Group("/commits", func() {
 					m.Get("", context.ReferencesGitRepo(), repo.GetAllCommits)
@@ -1170,24 +1114,24 @@ func Routes(ctx gocontext.Context) *web.Route {
 					m.Get("/tags/{sha}", repo.GetAnnotatedTag)
 					m.Get("/notes/{sha}", repo.GetNote)
 				}, context.ReferencesGitRepo(true), reqRepoReader(unit.TypeCode))
-				m.Post("/diffpatch", reqRepoWriter(unit.TypeCode), reqToken(auth_model.AccessTokenScopeRepo), bind(api.ApplyDiffPatchFileOptions{}), repo.ApplyDiffPatch)
+				m.Post("/diffpatch", reqRepoWriter(unit.TypeCode), reqToken(), bind(api.ApplyDiffPatchFileOptions{}), repo.ApplyDiffPatch)
 				m.Group("/contents", func() {
 					m.Get("", repo.GetContentsList)
-					m.Post("", reqToken(auth_model.AccessTokenScopeRepo), bind(api.ChangeFilesOptions{}), reqRepoBranchWriter, repo.ChangeFiles)
+					m.Post("", reqToken(), bind(api.ChangeFilesOptions{}), reqRepoBranchWriter, repo.ChangeFiles)
 					m.Get("/*", repo.GetContents)
 					m.Group("/*", func() {
 						m.Post("", bind(api.CreateFileOptions{}), reqRepoBranchWriter, repo.CreateFile)
 						m.Put("", bind(api.UpdateFileOptions{}), reqRepoBranchWriter, repo.UpdateFile)
 						m.Delete("", bind(api.DeleteFileOptions{}), reqRepoBranchWriter, repo.DeleteFile)
-					}, reqToken(auth_model.AccessTokenScopeRepo))
+					}, reqToken())
 				}, reqRepoReader(unit.TypeCode))
 				m.Get("/signing-key.gpg", misc.SigningKey)
 				m.Group("/topics", func() {
 					m.Combo("").Get(repo.ListTopics).
-						Put(reqToken(auth_model.AccessTokenScopeRepo), reqAdmin(), bind(api.RepoTopicOptions{}), repo.UpdateTopics)
+						Put(reqToken(), reqAdmin(), bind(api.RepoTopicOptions{}), repo.UpdateTopics)
 					m.Group("/{topic}", func() {
-						m.Combo("").Put(reqToken(auth_model.AccessTokenScopeRepo), repo.AddTopic).
-							Delete(reqToken(auth_model.AccessTokenScopeRepo), repo.DeleteTopic)
+						m.Combo("").Put(reqToken(), repo.AddTopic).
+							Delete(reqToken(), repo.DeleteTopic)
 					}, reqAdmin())
 				}, reqAnyRepoReader())
 				m.Get("/issue_templates", context.ReferencesGitRepo(), repo.GetIssueTemplates)
@@ -1197,54 +1141,177 @@ func Routes(ctx gocontext.Context) *web.Route {
 				m.Get("/activities/feeds", repo.ListRepoActivityFeeds)
 				m.Get("/new_pin_allowed", repo.AreNewIssuePinsAllowed)
 			}, repoAssignment())
-		})
+		}, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryRepository))
+
+		// Notifications (requires notifications scope)
+		m.Group("/repos", func() {
+			m.Group("/{username}/{reponame}", func() {
+				m.Combo("/notifications", reqToken()).
+					Get(notify.ListRepoNotifications).
+					Put(notify.ReadRepoNotifications)
+			}, repoAssignment())
+		}, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryNotification))
+
+		// Issue (requires issue scope)
+		m.Group("/repos", func() {
+			m.Get("/issues/search", repo.SearchIssues)
+
+			m.Group("/{username}/{reponame}", func() {
+				m.Group("/issues", func() {
+					m.Combo("").Get(repo.ListIssues).
+						Post(reqToken(), mustNotBeArchived, bind(api.CreateIssueOption{}), repo.CreateIssue)
+					m.Get("/pinned", 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, repo.CreateIssueCommentAttachment)
+								m.Combo("/{asset}").
+									Get(repo.GetIssueCommentAttachment).
+									Patch(reqToken(), mustNotBeArchived, bind(api.EditAttachmentOptions{}), repo.EditIssueCommentAttachment).
+									Delete(reqToken(), mustNotBeArchived, repo.DeleteIssueCommentAttachment)
+							}, mustEnableAttachments)
+						})
+					})
+					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()).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(), repo.ClearIssueLabels)
+							m.Delete("/{id}", reqToken(), 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)
+						}, reqToken())
+						m.Combo("/deadline").Post(reqToken(), bind(api.EditDeadlineOption{}), repo.UpdateIssueDeadline)
+						m.Group("/stopwatch", func() {
+							m.Post("/start", repo.StartIssueStopwatch)
+							m.Post("/stop", repo.StopIssueStopwatch)
+							m.Delete("/delete", repo.DeleteIssueStopwatch)
+						}, reqToken())
+						m.Group("/subscriptions", func() {
+							m.Get("", repo.GetIssueSubscribers)
+							m.Get("/check", reqToken(), repo.CheckIssueSubscription)
+							m.Put("/{user}", reqToken(), repo.AddIssueSubscription)
+							m.Delete("/{user}", reqToken(), repo.DelIssueSubscription)
+						})
+						m.Combo("/reactions").
+							Get(repo.GetIssueReactions).
+							Post(reqToken(), bind(api.EditReactionOption{}), repo.PostIssueReaction).
+							Delete(reqToken(), bind(api.EditReactionOption{}), repo.DeleteIssueReaction)
+						m.Group("/assets", func() {
+							m.Combo("").
+								Get(repo.ListIssueAttachments).
+								Post(reqToken(), mustNotBeArchived, repo.CreateIssueAttachment)
+							m.Combo("/{asset}").
+								Get(repo.GetIssueAttachment).
+								Patch(reqToken(), mustNotBeArchived, bind(api.EditAttachmentOptions{}), repo.EditIssueAttachment).
+								Delete(reqToken(), mustNotBeArchived, repo.DeleteIssueAttachment)
+						}, mustEnableAttachments)
+						m.Combo("/dependencies").
+							Get(repo.GetIssueDependencies).
+							Post(reqToken(), mustNotBeArchived, bind(api.IssueMeta{}), repo.CreateIssueDependency).
+							Delete(reqToken(), mustNotBeArchived, bind(api.IssueMeta{}), repo.RemoveIssueDependency)
+						m.Combo("/blocks").
+							Get(repo.GetIssueBlocks).
+							Post(reqToken(), bind(api.IssueMeta{}), repo.CreateIssueBlocking).
+							Delete(reqToken(), bind(api.IssueMeta{}), repo.RemoveIssueBlocking)
+						m.Group("/pin", func() {
+							m.Combo("").
+								Post(reqToken(), reqAdmin(), repo.PinIssue).
+								Delete(reqToken(), reqAdmin(), repo.UnpinIssue)
+							m.Patch("/{position}", reqToken(), reqAdmin(), repo.MoveIssuePin)
+						})
+					})
+				}, mustEnableIssuesOrPulls)
+				m.Group("/labels", func() {
+					m.Combo("").Get(repo.ListLabels).
+						Post(reqToken(), reqRepoWriter(unit.TypeIssues, unit.TypePullRequests), bind(api.CreateLabelOption{}), repo.CreateLabel)
+					m.Combo("/{id}").Get(repo.GetLabel).
+						Patch(reqToken(), reqRepoWriter(unit.TypeIssues, unit.TypePullRequests), bind(api.EditLabelOption{}), repo.EditLabel).
+						Delete(reqToken(), reqRepoWriter(unit.TypeIssues, unit.TypePullRequests), repo.DeleteLabel)
+				})
+				m.Group("/milestones", func() {
+					m.Combo("").Get(repo.ListMilestones).
+						Post(reqToken(), reqRepoWriter(unit.TypeIssues, unit.TypePullRequests), bind(api.CreateMilestoneOption{}), repo.CreateMilestone)
+					m.Combo("/{id}").Get(repo.GetMilestone).
+						Patch(reqToken(), reqRepoWriter(unit.TypeIssues, unit.TypePullRequests), bind(api.EditMilestoneOption{}), repo.EditMilestone).
+						Delete(reqToken(), reqRepoWriter(unit.TypeIssues, unit.TypePullRequests), repo.DeleteMilestone)
+				})
+			}, repoAssignment())
+		}, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryIssue))
 
 		// NOTE: these are Gitea package management API - see packages.CommonRoutes and packages.DockerContainerRoutes for endpoints that implement package manager APIs
 		m.Group("/packages/{username}", func() {
 			m.Group("/{type}/{name}/{version}", func() {
-				m.Get("", reqToken(auth_model.AccessTokenScopeReadPackage), packages.GetPackage)
-				m.Delete("", reqToken(auth_model.AccessTokenScopeDeletePackage), reqPackageAccess(perm.AccessModeWrite), packages.DeletePackage)
-				m.Get("/files", reqToken(auth_model.AccessTokenScopeReadPackage), packages.ListPackageFiles)
+				m.Get("", reqToken(), packages.GetPackage)
+				m.Delete("", reqToken(), reqPackageAccess(perm.AccessModeWrite), packages.DeletePackage)
+				m.Get("/files", reqToken(), packages.ListPackageFiles)
 			})
-			m.Get("/", reqToken(auth_model.AccessTokenScopeReadPackage), packages.ListPackages)
-		}, context_service.UserAssignmentAPI(), context.PackageAssignmentAPI(), reqPackageAccess(perm.AccessModeRead))
+			m.Get("/", reqToken(), packages.ListPackages)
+		}, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryPackage), context_service.UserAssignmentAPI(), context.PackageAssignmentAPI(), reqPackageAccess(perm.AccessModeRead))
 
 		// Organizations
-		m.Get("/user/orgs", reqToken(auth_model.AccessTokenScopeReadOrg), org.ListMyOrgs)
+		m.Get("/user/orgs", reqToken(), tokenRequiresScopes(auth_model.AccessTokenScopeCategoryUser, auth_model.AccessTokenScopeCategoryOrganization), org.ListMyOrgs)
 		m.Group("/users/{username}/orgs", func() {
-			m.Get("", reqToken(auth_model.AccessTokenScopeReadOrg), org.ListUserOrgs)
-			m.Get("/{org}/permissions", reqToken(auth_model.AccessTokenScopeReadOrg), org.GetUserOrgsPermissions)
-		}, context_service.UserAssignmentAPI())
-		m.Post("/orgs", reqToken(auth_model.AccessTokenScopeWriteOrg), bind(api.CreateOrgOption{}), org.Create)
-		m.Get("/orgs", org.GetAll)
+			m.Get("", reqToken(), org.ListUserOrgs)
+			m.Get("/{org}/permissions", reqToken(), org.GetUserOrgsPermissions)
+		}, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryUser, auth_model.AccessTokenScopeCategoryOrganization), context_service.UserAssignmentAPI())
+		m.Post("/orgs", tokenRequiresScopes(auth_model.AccessTokenScopeCategoryOrganization), reqToken(), bind(api.CreateOrgOption{}), org.Create)
+		m.Get("/orgs", org.GetAll, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryOrganization))
 		m.Group("/orgs/{org}", func() {
 			m.Combo("").Get(org.Get).
-				Patch(reqToken(auth_model.AccessTokenScopeWriteOrg), reqOrgOwnership(), bind(api.EditOrgOption{}), org.Edit).
-				Delete(reqToken(auth_model.AccessTokenScopeWriteOrg), reqOrgOwnership(), org.Delete)
+				Patch(reqToken(), reqOrgOwnership(), bind(api.EditOrgOption{}), org.Edit).
+				Delete(reqToken(), reqOrgOwnership(), org.Delete)
 			m.Combo("/repos").Get(user.ListOrgRepos).
-				Post(reqToken(auth_model.AccessTokenScopeWriteOrg), bind(api.CreateRepoOption{}), repo.CreateOrgRepo)
+				Post(reqToken(), bind(api.CreateRepoOption{}), repo.CreateOrgRepo)
 			m.Group("/members", func() {
-				m.Get("", reqToken(auth_model.AccessTokenScopeReadOrg), org.ListMembers)
-				m.Combo("/{username}").Get(reqToken(auth_model.AccessTokenScopeReadOrg), org.IsMember).
-					Delete(reqToken(auth_model.AccessTokenScopeWriteOrg), reqOrgOwnership(), org.DeleteMember)
+				m.Get("", reqToken(), org.ListMembers)
+				m.Combo("/{username}").Get(reqToken(), org.IsMember).
+					Delete(reqToken(), reqOrgOwnership(), org.DeleteMember)
 			})
 			m.Group("/public_members", func() {
 				m.Get("", org.ListPublicMembers)
 				m.Combo("/{username}").Get(org.IsPublicMember).
-					Put(reqToken(auth_model.AccessTokenScopeWriteOrg), reqOrgMembership(), org.PublicizeMember).
-					Delete(reqToken(auth_model.AccessTokenScopeWriteOrg), reqOrgMembership(), org.ConcealMember)
+					Put(reqToken(), reqOrgMembership(), org.PublicizeMember).
+					Delete(reqToken(), reqOrgMembership(), org.ConcealMember)
 			})
 			m.Group("/teams", func() {
-				m.Get("", reqToken(auth_model.AccessTokenScopeReadOrg), org.ListTeams)
-				m.Post("", reqToken(auth_model.AccessTokenScopeWriteOrg), reqOrgOwnership(), bind(api.CreateTeamOption{}), org.CreateTeam)
-				m.Get("/search", reqToken(auth_model.AccessTokenScopeReadOrg), org.SearchTeam)
+				m.Get("", reqToken(), org.ListTeams)
+				m.Post("", reqToken(), reqOrgOwnership(), bind(api.CreateTeamOption{}), org.CreateTeam)
+				m.Get("/search", reqToken(), org.SearchTeam)
 			}, reqOrgMembership())
 			m.Group("/labels", func() {
 				m.Get("", org.ListLabels)
-				m.Post("", reqToken(auth_model.AccessTokenScopeWriteOrg), reqOrgOwnership(), bind(api.CreateLabelOption{}), org.CreateLabel)
-				m.Combo("/{id}").Get(reqToken(auth_model.AccessTokenScopeReadOrg), org.GetLabel).
-					Patch(reqToken(auth_model.AccessTokenScopeWriteOrg), reqOrgOwnership(), bind(api.EditLabelOption{}), org.EditLabel).
-					Delete(reqToken(auth_model.AccessTokenScopeWriteOrg), reqOrgOwnership(), org.DeleteLabel)
+				m.Post("", reqToken(), reqOrgOwnership(), bind(api.CreateLabelOption{}), org.CreateLabel)
+				m.Combo("/{id}").Get(reqToken(), org.GetLabel).
+					Patch(reqToken(), reqOrgOwnership(), bind(api.EditLabelOption{}), org.EditLabel).
+					Delete(reqToken(), reqOrgOwnership(), org.DeleteLabel)
 			})
 			m.Group("/hooks", func() {
 				m.Combo("").Get(org.ListHooks).
@@ -1252,29 +1319,29 @@ func Routes(ctx gocontext.Context) *web.Route {
 				m.Combo("/{id}").Get(org.GetHook).
 					Patch(bind(api.EditHookOption{}), org.EditHook).
 					Delete(org.DeleteHook)
-			}, reqToken(auth_model.AccessTokenScopeAdminOrgHook), reqOrgOwnership(), reqWebhooksEnabled())
+			}, reqToken(), reqOrgOwnership(), reqWebhooksEnabled())
 			m.Get("/activities/feeds", org.ListOrgActivityFeeds)
-		}, orgAssignment(true))
+		}, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryOrganization), orgAssignment(true))
 		m.Group("/teams/{teamid}", func() {
-			m.Combo("").Get(reqToken(auth_model.AccessTokenScopeReadOrg), org.GetTeam).
-				Patch(reqToken(auth_model.AccessTokenScopeWriteOrg), reqOrgOwnership(), bind(api.EditTeamOption{}), org.EditTeam).
-				Delete(reqToken(auth_model.AccessTokenScopeWriteOrg), reqOrgOwnership(), org.DeleteTeam)
+			m.Combo("").Get(reqToken(), org.GetTeam).
+				Patch(reqToken(), reqOrgOwnership(), bind(api.EditTeamOption{}), org.EditTeam).
+				Delete(reqToken(), reqOrgOwnership(), org.DeleteTeam)
 			m.Group("/members", func() {
-				m.Get("", reqToken(auth_model.AccessTokenScopeReadOrg), org.GetTeamMembers)
+				m.Get("", reqToken(), org.GetTeamMembers)
 				m.Combo("/{username}").
-					Get(reqToken(auth_model.AccessTokenScopeReadOrg), org.GetTeamMember).
-					Put(reqToken(auth_model.AccessTokenScopeWriteOrg), reqOrgOwnership(), org.AddTeamMember).
-					Delete(reqToken(auth_model.AccessTokenScopeWriteOrg), reqOrgOwnership(), org.RemoveTeamMember)
+					Get(reqToken(), org.GetTeamMember).
+					Put(reqToken(), reqOrgOwnership(), org.AddTeamMember).
+					Delete(reqToken(), reqOrgOwnership(), org.RemoveTeamMember)
 			})
 			m.Group("/repos", func() {
-				m.Get("", reqToken(auth_model.AccessTokenScopeReadOrg), org.GetTeamRepos)
+				m.Get("", reqToken(), org.GetTeamRepos)
 				m.Combo("/{org}/{reponame}").
-					Put(reqToken(auth_model.AccessTokenScopeWriteOrg), org.AddTeamRepository).
-					Delete(reqToken(auth_model.AccessTokenScopeWriteOrg), org.RemoveTeamRepository).
-					Get(reqToken(auth_model.AccessTokenScopeReadOrg), org.GetTeamRepo)
+					Put(reqToken(), org.AddTeamRepository).
+					Delete(reqToken(), org.RemoveTeamRepository).
+					Get(reqToken(), org.GetTeamRepo)
 			})
 			m.Get("/activities/feeds", org.ListTeamActivityFeeds)
-		}, orgAssignment(false, true), reqToken(""), reqTeamMembership())
+		}, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryOrganization), orgAssignment(false, true), reqToken(), reqTeamMembership())
 
 		m.Group("/admin", func() {
 			m.Group("/cron", func() {
@@ -1314,11 +1381,11 @@ func Routes(ctx gocontext.Context) *web.Route {
 					Patch(bind(api.EditHookOption{}), admin.EditHook).
 					Delete(admin.DeleteHook)
 			})
-		}, reqToken(auth_model.AccessTokenScopeSudo), reqSiteAdmin())
+		}, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryAdmin), reqToken(), reqSiteAdmin())
 
 		m.Group("/topics", func() {
 			m.Get("/search", repo.TopicSearch)
-		})
+		}, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryRepository))
 	}, sudo())
 
 	return m
diff --git a/routers/web/repo/http.go b/routers/web/repo/http.go
index b6ebd25915..f4e9ac86a1 100644
--- a/routers/web/repo/http.go
+++ b/routers/web/repo/http.go
@@ -152,7 +152,7 @@ func httpBase(ctx *context.Context) (h *serviceHandler) {
 			return
 		}
 
-		context.CheckRepoScopedToken(ctx, repo)
+		context.CheckRepoScopedToken(ctx, repo, auth_model.GetScopeLevelFromAccessMode(accessMode))
 		if ctx.Written() {
 			return
 		}
diff --git a/routers/web/user/setting/applications.go b/routers/web/user/setting/applications.go
index ac935e51bb..f9e9ca5e52 100644
--- a/routers/web/user/setting/applications.go
+++ b/routers/web/user/setting/applications.go
@@ -89,6 +89,7 @@ func DeleteApplication(ctx *context.Context) {
 }
 
 func loadApplicationsData(ctx *context.Context) {
+	ctx.Data["AccessTokenScopePublicOnly"] = auth_model.AccessTokenScopePublicOnly
 	tokens, err := auth_model.ListAccessTokens(auth_model.ListAccessTokensOptions{UserID: ctx.Doer.ID})
 	if err != nil {
 		ctx.ServerError("ListAccessTokens", err)
@@ -96,6 +97,7 @@ func loadApplicationsData(ctx *context.Context) {
 	}
 	ctx.Data["Tokens"] = tokens
 	ctx.Data["EnableOAuth2"] = setting.OAuth2.Enable
+	ctx.Data["IsAdmin"] = ctx.Doer.IsAdmin
 	if setting.OAuth2.Enable {
 		ctx.Data["Applications"], err = auth_model.GetOAuth2ApplicationsByUserID(ctx, ctx.Doer.ID)
 		if err != nil {
diff --git a/services/forms/user_form_test.go b/services/forms/user_form_test.go
index 84efa25d53..66050187c9 100644
--- a/services/forms/user_form_test.go
+++ b/services/forms/user_form_test.go
@@ -112,12 +112,12 @@ func TestNewAccessTokenForm_GetScope(t *testing.T) {
 		expectedErr error
 	}{
 		{
-			form:  NewAccessTokenForm{Name: "test", Scope: []string{"repo"}},
-			scope: "repo",
+			form:  NewAccessTokenForm{Name: "test", Scope: []string{"read:repository"}},
+			scope: "read:repository",
 		},
 		{
-			form:  NewAccessTokenForm{Name: "test", Scope: []string{"repo", "user"}},
-			scope: "repo,user",
+			form:  NewAccessTokenForm{Name: "test", Scope: []string{"read:repository", "write:user"}},
+			scope: "read:repository,write:user",
 		},
 	}
 
diff --git a/services/lfs/locks.go b/services/lfs/locks.go
index 1e5db6bd20..08d7432656 100644
--- a/services/lfs/locks.go
+++ b/services/lfs/locks.go
@@ -8,6 +8,7 @@ import (
 	"strconv"
 	"strings"
 
+	auth_model "code.gitea.io/gitea/models/auth"
 	git_model "code.gitea.io/gitea/models/git"
 	repo_model "code.gitea.io/gitea/models/repo"
 	"code.gitea.io/gitea/modules/context"
@@ -58,7 +59,7 @@ func GetListLockHandler(ctx *context.Context) {
 	}
 	repository.MustOwner(ctx)
 
-	context.CheckRepoScopedToken(ctx, repository)
+	context.CheckRepoScopedToken(ctx, repository, auth_model.Read)
 	if ctx.Written() {
 		return
 	}
@@ -150,7 +151,7 @@ func PostLockHandler(ctx *context.Context) {
 	}
 	repository.MustOwner(ctx)
 
-	context.CheckRepoScopedToken(ctx, repository)
+	context.CheckRepoScopedToken(ctx, repository, auth_model.Write)
 	if ctx.Written() {
 		return
 	}
@@ -222,7 +223,7 @@ func VerifyLockHandler(ctx *context.Context) {
 	}
 	repository.MustOwner(ctx)
 
-	context.CheckRepoScopedToken(ctx, repository)
+	context.CheckRepoScopedToken(ctx, repository, auth_model.Read)
 	if ctx.Written() {
 		return
 	}
@@ -293,7 +294,7 @@ func UnLockHandler(ctx *context.Context) {
 	}
 	repository.MustOwner(ctx)
 
-	context.CheckRepoScopedToken(ctx, repository)
+	context.CheckRepoScopedToken(ctx, repository, auth_model.Write)
 	if ctx.Written() {
 		return
 	}
diff --git a/services/lfs/server.go b/services/lfs/server.go
index 64e1203394..1f82aed54b 100644
--- a/services/lfs/server.go
+++ b/services/lfs/server.go
@@ -18,6 +18,7 @@ import (
 	"strings"
 
 	actions_model "code.gitea.io/gitea/models/actions"
+	auth_model "code.gitea.io/gitea/models/auth"
 	git_model "code.gitea.io/gitea/models/git"
 	"code.gitea.io/gitea/models/perm"
 	access_model "code.gitea.io/gitea/models/perm/access"
@@ -423,7 +424,12 @@ func getAuthenticatedRepository(ctx *context.Context, rc *requestContext, requir
 		return nil
 	}
 
-	context.CheckRepoScopedToken(ctx, repository)
+	if requireWrite {
+		context.CheckRepoScopedToken(ctx, repository, auth_model.Write)
+	} else {
+		context.CheckRepoScopedToken(ctx, repository, auth_model.Read)
+	}
+
 	if ctx.Written() {
 		return nil
 	}
diff --git a/templates/user/settings/applications.tmpl b/templates/user/settings/applications.tmpl
index 1c12a0a2b7..2b7db82dae 100644
--- a/templates/user/settings/applications.tmpl
+++ b/templates/user/settings/applications.tmpl
@@ -18,13 +18,21 @@
 						</div>
 						<i class="text {{if .HasRecentActivity}}green{{end}}" {{if .HasRecentActivity}}data-tooltip-content="{{$.locale.Tr "settings.token_state_desc"}}"{{end}}>{{svg "fontawesome-send" 36}}</i>
 						<div class="content">
-							<!--Temporarily disable-->
-							<strong>{{.Name}}</strong>
-							<details class="gt-hidden"><summary><strong>{{.Name}}</strong></summary>
-								<p class="gt-my-2">{{$.locale.Tr "settings.scopes_list"}}</p>
+							<details><summary><strong>{{.Name}}</strong></summary>
+								<p class="gt-my-2">
+									{{$.locale.Tr "settings.repo_and_org_access"}}:
+									{{if .DisplayPublicOnly}}
+										{{$.locale.Tr "settings.permissions_public_only"}}
+									{{else}}
+										{{$.locale.Tr "settings.permissions_access_all"}}
+									{{end}}
+								</p>
+								<p class="gt-my-2">{{$.locale.Tr "settings.permissions_list"}}</p>
 								<ul class="gt-my-2">
 								{{range .Scope.StringSlice}}
-									<li>{{.}}</li>
+									{{if (ne . $.AccessTokenScopePublicOnly)}}
+										<li>{{.}}</li>
+									{{end}}
 								{{end}}
 								</ul>
 							</details>
@@ -40,222 +48,46 @@
 			<h5 class="ui top header">
 				{{.locale.Tr "settings.generate_new_token"}}
 			</h5>
-			<p>{{.locale.Tr "settings.new_token_desc"}}</p>
-			<form class="ui form ignore-dirty" action="{{.Link}}" method="post">
+			<form id="scoped-access-form" class="ui form ignore-dirty" action="{{.Link}}" method="post">
 				{{.CsrfTokenHtml}}
 				<div class="field {{if .Err_Name}}error{{end}}">
 					<label for="name">{{.locale.Tr "settings.token_name"}}</label>
 					<input id="name" name="name" value="{{.name}}" autofocus required maxlength="255">
 				</div>
-				<!--Temporarily disable-->
-				<details class="gt-hidden ui optional field">
-					<summary class="gt-p-2">
-						{{.locale.Tr "settings.select_scopes"}}
+				<div class="field">
+					<label>{{.locale.Tr "settings.repo_and_org_access"}}</label>
+					<label class="gt-cursor-pointer">
+						<input class="enable-system gt-mt-2 gt-mr-2" type="radio" name="scope" value="{{$.AccessTokenScopePublicOnly}}">
+						{{.locale.Tr "settings.permissions_public_only"}}
+					</label>
+					<label class="gt-cursor-pointer">
+						<input class="enable-system gt-mt-2 gt-mr-2" type="radio" name="scope" value="" checked>
+						{{.locale.Tr "settings.permissions_access_all"}}
+					</label>
+				</div>
+				<details class="ui optional field">
+					<summary class="gt-pb-4 gt-pl-2">
+						{{.locale.Tr "settings.select_permissions"}}
 					</summary>
-					<div class="field gt-pl-2">
-						<div class="ui checkbox">
-							<input class="enable-system" type="checkbox" name="scope" value="repo">
-							<label>repo</label>
-						</div>
-					</div>
-					<div class="field gt-pl-4">
-						<div class="field">
-							<div class="ui checkbox">
-								<input class="enable-system" type="checkbox" name="scope" value="repo:status">
-								<label>repo:status</label>
-							</div>
-						</div>
-						<div class="field">
-							<div class="ui checkbox">
-								<input class="enable-system" type="checkbox" name="scope" value="public_repo">
-								<label>public_repo</label>
-							</div>
-						</div>
-					</div>
-					<div class="field">
-						<div class="ui checkbox">
-							<input class="enable-system" type="checkbox" name="scope" value="admin:org">
-							<label>admin:org</label>
-						</div>
-					</div>
-					<div class="field gt-pl-4">
-						<div class="field">
-							<div class="ui checkbox">
-								<input class="enable-system" type="checkbox" name="scope" value="write:org">
-								<label>write:org</label>
-							</div>
-						</div>
-						<div class="field">
-							<div class="ui checkbox">
-								<input class="enable-system" type="checkbox" name="scope" value="read:org">
-								<label>read:org</label>
-							</div>
-						</div>
-					</div>
-					<div class="field">
-						<div class="ui checkbox">
-							<input class="enable-system" type="checkbox" name="scope" value="admin:public_key">
-							<label>admin:public_key</label>
-						</div>
-					</div>
-					<div class="field gt-pl-4">
-						<div class="field">
-							<div class="ui checkbox">
-								<input class="enable-system" type="checkbox" name="scope" value="write:public_key">
-								<label>write:public_key</label>
-							</div>
-						</div>
-						<div class="field">
-							<div class="ui checkbox">
-								<input class="enable-system" type="checkbox" name="scope" value="read:public_key">
-								<label>read:public_key</label>
-							</div>
-						</div>
-					</div>
-					<div class="field">
-						<div class="ui checkbox">
-							<input class="enable-system" type="checkbox" name="scope" value="admin:repo_hook">
-							<label>admin:repo_hook</label>
-						</div>
-					</div>
-					<div class="field gt-pl-4">
-						<div class="field">
-							<div class="ui checkbox">
-								<input class="enable-system" type="checkbox" name="scope" value="write:repo_hook">
-								<label>write:repo_hook</label>
-							</div>
-						</div>
-						<div class="field">
-							<div class="ui checkbox">
-								<input class="enable-system" type="checkbox" name="scope" value="read:repo_hook">
-								<label>read:repo_hook</label>
-							</div>
-						</div>
-					</div>
-					<div class="field">
-						<div class="ui checkbox">
-							<input class="enable-system" type="checkbox" name="scope" value="admin:org_hook">
-							<label>admin:org_hook</label>
-						</div>
-					</div>
-					<div class="field">
-						<div class="ui checkbox">
-							<input class="enable-system" type="checkbox" name="scope" value="admin:user_hook">
-							<label>admin:user_hook</label>
-						</div>
-					</div>
-					<div class="field">
-						<div class="ui checkbox">
-							<input class="enable-system" type="checkbox" name="scope" value="notification">
-							<label>notification</label>
-						</div>
-					</div>
-					<div class="field">
-						<div class="ui checkbox">
-							<input class="enable-system" type="checkbox" name="scope" value="user">
-							<label>user</label>
-						</div>
-					</div>
-					<div class="field gt-pl-4">
-						<div class="field">
-							<div class="ui checkbox">
-								<input class="enable-system" type="checkbox" name="scope" value="read:user">
-								<label>read:user</label>
-							</div>
-						</div>
-						<div class="field">
-							<div class="ui checkbox">
-								<input class="enable-system" type="checkbox" name="scope" value="user:email">
-								<label>user:email</label>
-							</div>
-						</div>
-						<div class="field">
-							<div class="ui checkbox">
-								<input class="enable-system" type="checkbox" name="scope" value="user:follow">
-								<label>user:follow</label>
-							</div>
-						</div>
-					</div>
-					<div class="field">
-						<div class="ui checkbox">
-							<input class="enable-system" type="checkbox" name="scope" value="delete_repo">
-							<label>delete_repo</label>
-						</div>
-					</div>
-					<div class="field">
-						<div class="ui checkbox">
-							<input class="enable-system" type="checkbox" name="scope" value="package">
-							<label>package</label>
-						</div>
-					</div>
-					<div class="field gt-pl-4">
-						<div class="field">
-							<div class="ui checkbox">
-								<input class="enable-system" type="checkbox" name="scope" value="write:package">
-								<label>write:package</label>
-							</div>
-						</div>
-						<div class="field">
-							<div class="ui checkbox">
-								<input class="enable-system" type="checkbox" name="scope" value="read:package">
-								<label>read:package</label>
-							</div>
-						</div>
-						<div class="field">
-							<div class="ui checkbox">
-								<input class="enable-system" type="checkbox" name="scope" value="delete:package">
-								<label>delete:package</label>
-							</div>
-						</div>
-					</div>
-					<div class="field">
-						<div class="ui checkbox">
-							<input class="enable-system" type="checkbox" name="scope" value="admin:gpg_key">
-							<label>admin:gpg_key</label>
-						</div>
-					</div>
-					<div class="field gt-pl-4">
-						<div class="field">
-							<div class="ui checkbox">
-								<input class="enable-system" type="checkbox" name="scope" value="write:gpg_key">
-								<label>write:gpg_key</label>
-							</div>
-						</div>
-						<div class="field">
-							<div class="ui checkbox">
-								<input class="enable-system" type="checkbox" name="scope" value="read:gpg_key">
-								<label>read:gpg_key</label>
-							</div>
-						</div>
-					</div>
-					<div class="field">
-						<div class="ui checkbox">
-							<input class="enable-system" type="checkbox" name="scope" value="admin:application">
-							<label>admin:application</label>
-						</div>
-					</div>
-					<div class="field gt-pl-4">
-						<div class="field">
-							<div class="ui checkbox">
-								<input class="enable-system" type="checkbox" name="scope" value="write:application">
-								<label>write:application</label>
-							</div>
-						</div>
-						<div class="field">
-							<div class="ui checkbox">
-								<input class="enable-system" type="checkbox" name="scope" value="read:application">
-								<label>read:application</label>
-							</div>
-						</div>
-					</div>
-					<div class="field">
-						<div class="ui checkbox">
-							<input class="enable-system" type="checkbox" name="scope" value="sudo">
-							<label>sudo</label>
-						</div>
+					<div class="activity meta">
+						<i>{{$.locale.Tr "settings.scoped_token_desc" (printf `href="/api/swagger" target="_blank"`) (printf `href="https://docs.gitea.com/development/oauth2-provider#scopes" target="_blank"`) | Str2html}}</i>
 					</div>
+					<scoped-access-token-category category="activitypub"></scoped-access-token-category>
+					{{if .IsAdmin}}
+						<scoped-access-token-category category="admin"></scoped-access-token-category>
+					{{end}}
+					<scoped-access-token-category category="issue"></scoped-access-token-category>
+					<scoped-access-token-category category="misc"></scoped-access-token-category>
+					<scoped-access-token-category category="notification"></scoped-access-token-category>
+					<scoped-access-token-category category="organization"></scoped-access-token-category>
+					<scoped-access-token-category category="package"></scoped-access-token-category>
+					<scoped-access-token-category category="repository"></scoped-access-token-category>
+					<scoped-access-token-category category="user"></scoped-access-token-category>
 				</details>
-				<button class="ui green button">
+				<div id="scoped-access-warning" class="ui warning message center gt-db gt-hidden">
+					{{.locale.Tr "settings.at_least_one_permission"}}
+				</div>
+				<button id="scoped-access-submit" class="ui green button">
 					{{.locale.Tr "settings.generate_token"}}
 				</button>
 			</form>
diff --git a/tests/integration/api_admin_org_test.go b/tests/integration/api_admin_org_test.go
index 89617f7a2c..0bf4b1f7cb 100644
--- a/tests/integration/api_admin_org_test.go
+++ b/tests/integration/api_admin_org_test.go
@@ -21,7 +21,7 @@ import (
 func TestAPIAdminOrgCreate(t *testing.T) {
 	onGiteaRun(t, func(*testing.T, *url.URL) {
 		session := loginUser(t, "user1")
-		token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeSudo)
+		token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteAdmin)
 
 		org := api.CreateOrgOption{
 			UserName:    "user2_org",
@@ -55,7 +55,7 @@ func TestAPIAdminOrgCreate(t *testing.T) {
 func TestAPIAdminOrgCreateBadVisibility(t *testing.T) {
 	onGiteaRun(t, func(*testing.T, *url.URL) {
 		session := loginUser(t, "user1")
-		token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeSudo)
+		token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteAdmin)
 
 		org := api.CreateOrgOption{
 			UserName:    "user2_org",
diff --git a/tests/integration/api_admin_test.go b/tests/integration/api_admin_test.go
index 7cfc3276ee..5900811922 100644
--- a/tests/integration/api_admin_test.go
+++ b/tests/integration/api_admin_test.go
@@ -25,7 +25,7 @@ func TestAPIAdminCreateAndDeleteSSHKey(t *testing.T) {
 	session := loginUser(t, "user1")
 	keyOwner := unittest.AssertExistsAndLoadBean(t, &user_model.User{Name: "user2"})
 
-	token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeSudo)
+	token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteAdmin)
 	urlStr := fmt.Sprintf("/api/v1/admin/users/%s/keys?token=%s", keyOwner.Name, token)
 	req := NewRequestWithValues(t, "POST", urlStr, map[string]string{
 		"key":   "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQC4cn+iXnA4KvcQYSV88vGn0Yi91vG47t1P7okprVmhNTkipNRIHWr6WdCO4VDr/cvsRkuVJAsLO2enwjGWWueOO6BodiBgyAOZ/5t5nJNMCNuLGT5UIo/RI1b0WRQwxEZTRjt6mFNw6lH14wRd8ulsr9toSWBPMOGWoYs1PDeDL0JuTjL+tr1SZi/EyxCngpYszKdXllJEHyI79KQgeD0Vt3pTrkbNVTOEcCNqZePSVmUH8X8Vhugz3bnE0/iE9Pb5fkWO9c4AnM1FgI/8Bvp27Fw2ShryIXuR6kKvUqhVMTuOSDHwu6A8jLE5Owt3GAYugDpDYuwTVNGrHLXKpPzrGGPE/jPmaLCMZcsdkec95dYeU3zKODEm8UQZFhmJmDeWVJ36nGrGZHL4J5aTTaeFUJmmXDaJYiJ+K2/ioKgXqnXvltu0A9R8/LGy4nrTJRr4JMLuJFoUXvGm1gXQ70w2LSpk6yl71RNC0hCtsBe8BP8IhYCM0EP5jh7eCMQZNvM= nocomment\n",
@@ -52,7 +52,7 @@ func TestAPIAdminDeleteMissingSSHKey(t *testing.T) {
 	defer tests.PrepareTestEnv(t)()
 
 	// user1 is an admin user
-	token := getUserToken(t, "user1", auth_model.AccessTokenScopeSudo)
+	token := getUserToken(t, "user1", auth_model.AccessTokenScopeWriteAdmin)
 	req := NewRequestf(t, "DELETE", "/api/v1/admin/users/user1/keys/%d?token=%s", unittest.NonexistentID, token)
 	MakeRequest(t, req, http.StatusNotFound)
 }
@@ -61,7 +61,7 @@ func TestAPIAdminDeleteUnauthorizedKey(t *testing.T) {
 	defer tests.PrepareTestEnv(t)()
 	adminUsername := "user1"
 	normalUsername := "user2"
-	token := getUserToken(t, adminUsername, auth_model.AccessTokenScopeSudo)
+	token := getUserToken(t, adminUsername, auth_model.AccessTokenScopeWriteAdmin)
 
 	urlStr := fmt.Sprintf("/api/v1/admin/users/%s/keys?token=%s", adminUsername, token)
 	req := NewRequestWithValues(t, "POST", urlStr, map[string]string{
@@ -82,7 +82,7 @@ func TestAPISudoUser(t *testing.T) {
 	defer tests.PrepareTestEnv(t)()
 	adminUsername := "user1"
 	normalUsername := "user2"
-	token := getUserToken(t, adminUsername, auth_model.AccessTokenScopeSudo)
+	token := getUserToken(t, adminUsername, auth_model.AccessTokenScopeReadUser)
 
 	urlStr := fmt.Sprintf("/api/v1/user?sudo=%s&token=%s", normalUsername, token)
 	req := NewRequest(t, "GET", urlStr)
@@ -98,7 +98,7 @@ func TestAPISudoUserForbidden(t *testing.T) {
 	adminUsername := "user1"
 	normalUsername := "user2"
 
-	token := getUserToken(t, normalUsername, auth_model.AccessTokenScopeSudo)
+	token := getUserToken(t, normalUsername, auth_model.AccessTokenScopeReadAdmin)
 	urlStr := fmt.Sprintf("/api/v1/user?sudo=%s&token=%s", adminUsername, token)
 	req := NewRequest(t, "GET", urlStr)
 	MakeRequest(t, req, http.StatusForbidden)
@@ -107,7 +107,7 @@ func TestAPISudoUserForbidden(t *testing.T) {
 func TestAPIListUsers(t *testing.T) {
 	defer tests.PrepareTestEnv(t)()
 	adminUsername := "user1"
-	token := getUserToken(t, adminUsername, auth_model.AccessTokenScopeSudo)
+	token := getUserToken(t, adminUsername, auth_model.AccessTokenScopeReadAdmin)
 
 	urlStr := fmt.Sprintf("/api/v1/admin/users?token=%s", token)
 	req := NewRequest(t, "GET", urlStr)
@@ -143,7 +143,7 @@ func TestAPIListUsersNonAdmin(t *testing.T) {
 func TestAPICreateUserInvalidEmail(t *testing.T) {
 	defer tests.PrepareTestEnv(t)()
 	adminUsername := "user1"
-	token := getUserToken(t, adminUsername, auth_model.AccessTokenScopeSudo)
+	token := getUserToken(t, adminUsername, auth_model.AccessTokenScopeWriteAdmin)
 	urlStr := fmt.Sprintf("/api/v1/admin/users?token=%s", token)
 	req := NewRequestWithValues(t, "POST", urlStr, map[string]string{
 		"email":                "invalid_email@domain.com\r\n",
@@ -161,7 +161,7 @@ func TestAPICreateUserInvalidEmail(t *testing.T) {
 func TestAPICreateAndDeleteUser(t *testing.T) {
 	defer tests.PrepareTestEnv(t)()
 	adminUsername := "user1"
-	token := getUserToken(t, adminUsername, auth_model.AccessTokenScopeSudo)
+	token := getUserToken(t, adminUsername, auth_model.AccessTokenScopeWriteAdmin)
 
 	req := NewRequestWithValues(
 		t,
@@ -187,7 +187,7 @@ func TestAPICreateAndDeleteUser(t *testing.T) {
 func TestAPIEditUser(t *testing.T) {
 	defer tests.PrepareTestEnv(t)()
 	adminUsername := "user1"
-	token := getUserToken(t, adminUsername, auth_model.AccessTokenScopeSudo)
+	token := getUserToken(t, adminUsername, auth_model.AccessTokenScopeWriteAdmin)
 	urlStr := fmt.Sprintf("/api/v1/admin/users/%s?token=%s", "user2", token)
 
 	req := NewRequestWithValues(t, "PATCH", urlStr, map[string]string{
@@ -229,7 +229,7 @@ func TestAPIEditUser(t *testing.T) {
 func TestAPICreateRepoForUser(t *testing.T) {
 	defer tests.PrepareTestEnv(t)()
 	adminUsername := "user1"
-	token := getUserToken(t, adminUsername, auth_model.AccessTokenScopeSudo)
+	token := getUserToken(t, adminUsername, auth_model.AccessTokenScopeWriteAdmin)
 
 	req := NewRequestWithJSON(
 		t,
@@ -245,7 +245,7 @@ func TestAPICreateRepoForUser(t *testing.T) {
 func TestAPIRenameUser(t *testing.T) {
 	defer tests.PrepareTestEnv(t)()
 	adminUsername := "user1"
-	token := getUserToken(t, adminUsername, auth_model.AccessTokenScopeSudo)
+	token := getUserToken(t, adminUsername, auth_model.AccessTokenScopeWriteAdmin)
 	urlStr := fmt.Sprintf("/api/v1/admin/users/%s/rename?token=%s", "user2", token)
 	req := NewRequestWithValues(t, "POST", urlStr, map[string]string{
 		// required
diff --git a/tests/integration/api_branch_test.go b/tests/integration/api_branch_test.go
index 6616399a8a..dd81ec22dd 100644
--- a/tests/integration/api_branch_test.go
+++ b/tests/integration/api_branch_test.go
@@ -16,7 +16,7 @@ import (
 )
 
 func testAPIGetBranch(t *testing.T, branchName string, exists bool) {
-	token := getUserToken(t, "user2", auth_model.AccessTokenScopeRepo)
+	token := getUserToken(t, "user2", auth_model.AccessTokenScopeReadRepository)
 	req := NewRequestf(t, "GET", "/api/v1/repos/user2/repo1/branches/%s?token=%s", branchName, token)
 	resp := MakeRequest(t, req, NoExpectedStatus)
 	if !exists {
@@ -32,7 +32,7 @@ func testAPIGetBranch(t *testing.T, branchName string, exists bool) {
 }
 
 func testAPIGetBranchProtection(t *testing.T, branchName string, expectedHTTPStatus int) {
-	token := getUserToken(t, "user2", auth_model.AccessTokenScopeRepo)
+	token := getUserToken(t, "user2", auth_model.AccessTokenScopeReadRepository)
 	req := NewRequestf(t, "GET", "/api/v1/repos/user2/repo1/branch_protections/%s?token=%s", branchName, token)
 	resp := MakeRequest(t, req, expectedHTTPStatus)
 
@@ -44,7 +44,7 @@ func testAPIGetBranchProtection(t *testing.T, branchName string, expectedHTTPSta
 }
 
 func testAPICreateBranchProtection(t *testing.T, branchName string, expectedHTTPStatus int) {
-	token := getUserToken(t, "user2", auth_model.AccessTokenScopeRepo)
+	token := getUserToken(t, "user2", auth_model.AccessTokenScopeWriteRepository)
 	req := NewRequestWithJSON(t, "POST", "/api/v1/repos/user2/repo1/branch_protections?token="+token, &api.BranchProtection{
 		RuleName: branchName,
 	})
@@ -58,7 +58,7 @@ func testAPICreateBranchProtection(t *testing.T, branchName string, expectedHTTP
 }
 
 func testAPIEditBranchProtection(t *testing.T, branchName string, body *api.BranchProtection, expectedHTTPStatus int) {
-	token := getUserToken(t, "user2", auth_model.AccessTokenScopeRepo)
+	token := getUserToken(t, "user2", auth_model.AccessTokenScopeWriteRepository)
 	req := NewRequestWithJSON(t, "PATCH", "/api/v1/repos/user2/repo1/branch_protections/"+branchName+"?token="+token, body)
 	resp := MakeRequest(t, req, expectedHTTPStatus)
 
@@ -70,13 +70,13 @@ func testAPIEditBranchProtection(t *testing.T, branchName string, body *api.Bran
 }
 
 func testAPIDeleteBranchProtection(t *testing.T, branchName string, expectedHTTPStatus int) {
-	token := getUserToken(t, "user2", auth_model.AccessTokenScopeRepo)
+	token := getUserToken(t, "user2", auth_model.AccessTokenScopeWriteRepository)
 	req := NewRequestf(t, "DELETE", "/api/v1/repos/user2/repo1/branch_protections/%s?token=%s", branchName, token)
 	MakeRequest(t, req, expectedHTTPStatus)
 }
 
 func testAPIDeleteBranch(t *testing.T, branchName string, expectedHTTPStatus int) {
-	token := getUserToken(t, "user2", auth_model.AccessTokenScopeRepo)
+	token := getUserToken(t, "user2", auth_model.AccessTokenScopeWriteRepository)
 	req := NewRequestf(t, "DELETE", "/api/v1/repos/user2/repo1/branches/%s?token=%s", branchName, token)
 	MakeRequest(t, req, expectedHTTPStatus)
 }
@@ -102,7 +102,7 @@ func TestAPICreateBranch(t *testing.T) {
 
 func testAPICreateBranches(t *testing.T, giteaURL *url.URL) {
 	username := "user2"
-	ctx := NewAPITestContext(t, username, "my-noo-repo", auth_model.AccessTokenScopeRepo)
+	ctx := NewAPITestContext(t, username, "my-noo-repo", auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser)
 	giteaURL.Path = ctx.GitPath()
 
 	t.Run("CreateRepo", doAPICreateRepository(ctx, false))
@@ -149,7 +149,7 @@ func testAPICreateBranches(t *testing.T, giteaURL *url.URL) {
 }
 
 func testAPICreateBranch(t testing.TB, session *TestSession, user, repo, oldBranch, newBranch string, status int) bool {
-	token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeRepo)
+	token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository)
 	req := NewRequestWithJSON(t, "POST", "/api/v1/repos/"+user+"/"+repo+"/branches?token="+token, &api.CreateBranchRepoOption{
 		BranchName:    newBranch,
 		OldBranchName: oldBranch,
diff --git a/tests/integration/api_comment_attachment_test.go b/tests/integration/api_comment_attachment_test.go
index 1d0b902b39..8aa73dd368 100644
--- a/tests/integration/api_comment_attachment_test.go
+++ b/tests/integration/api_comment_attachment_test.go
@@ -36,8 +36,8 @@ func TestAPIGetCommentAttachment(t *testing.T) {
 	repoOwner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
 
 	session := loginUser(t, repoOwner.Name)
-	token := getTokenForLoggedInUser(t, session)
-	req := NewRequestf(t, "GET", "/api/v1/repos/%s/%s/issues/comments/%d/assets/%d", repoOwner.Name, repo.Name, comment.ID, attachment.ID)
+	token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeReadIssue)
+	req := NewRequestf(t, "GET", "/api/v1/repos/%s/%s/issues/comments/%d/assets/%d?token=%s", repoOwner.Name, repo.Name, comment.ID, attachment.ID, token)
 	session.MakeRequest(t, req, http.StatusOK)
 	req = NewRequestf(t, "GET", "/api/v1/repos/%s/%s/issues/comments/%d/assets/%d?token=%s", repoOwner.Name, repo.Name, comment.ID, attachment.ID, token)
 	resp := session.MakeRequest(t, req, http.StatusOK)
@@ -61,8 +61,9 @@ func TestAPIListCommentAttachments(t *testing.T) {
 	repoOwner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
 
 	session := loginUser(t, repoOwner.Name)
-	req := NewRequestf(t, "GET", "/api/v1/repos/%s/%s/issues/comments/%d/assets",
-		repoOwner.Name, repo.Name, comment.ID)
+	token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeReadIssue)
+	req := NewRequestf(t, "GET", "/api/v1/repos/%s/%s/issues/comments/%d/assets?token=%s",
+		repoOwner.Name, repo.Name, comment.ID, token)
 	resp := session.MakeRequest(t, req, http.StatusOK)
 
 	var apiAttachments []*api.Attachment
@@ -82,7 +83,7 @@ func TestAPICreateCommentAttachment(t *testing.T) {
 	repoOwner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
 
 	session := loginUser(t, repoOwner.Name)
-	token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeRepo)
+	token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteIssue)
 	urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/issues/comments/%d/assets?token=%s",
 		repoOwner.Name, repo.Name, comment.ID, token)
 
@@ -121,7 +122,7 @@ func TestAPIEditCommentAttachment(t *testing.T) {
 	repoOwner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
 
 	session := loginUser(t, repoOwner.Name)
-	token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeRepo)
+	token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteIssue)
 	urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/issues/comments/%d/assets/%d?token=%s",
 		repoOwner.Name, repo.Name, comment.ID, attachment.ID, token)
 	req := NewRequestWithValues(t, "PATCH", urlStr, map[string]string{
@@ -144,7 +145,7 @@ func TestAPIDeleteCommentAttachment(t *testing.T) {
 	repoOwner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
 
 	session := loginUser(t, repoOwner.Name)
-	token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeRepo)
+	token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteIssue)
 	urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/issues/comments/%d/assets/%d?token=%s",
 		repoOwner.Name, repo.Name, comment.ID, attachment.ID, token)
 
diff --git a/tests/integration/api_comment_test.go b/tests/integration/api_comment_test.go
index 56de8b5909..c773afab3d 100644
--- a/tests/integration/api_comment_test.go
+++ b/tests/integration/api_comment_test.go
@@ -76,7 +76,7 @@ func TestAPIListIssueComments(t *testing.T) {
 	repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: issue.RepoID})
 	repoOwner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
 
-	token := getUserToken(t, repoOwner.Name, auth_model.AccessTokenScopeRepo)
+	token := getUserToken(t, repoOwner.Name, auth_model.AccessTokenScopeReadIssue)
 	req := NewRequestf(t, "GET", "/api/v1/repos/%s/%s/issues/%d/comments?token=%s",
 		repoOwner.Name, repo.Name, issue.Index, token)
 	resp := MakeRequest(t, req, http.StatusOK)
@@ -96,7 +96,7 @@ func TestAPICreateComment(t *testing.T) {
 	repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: issue.RepoID})
 	repoOwner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
 
-	token := getUserToken(t, repoOwner.Name, auth_model.AccessTokenScopeRepo)
+	token := getUserToken(t, repoOwner.Name, auth_model.AccessTokenScopeWriteIssue)
 	urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/issues/%d/comments?token=%s",
 		repoOwner.Name, repo.Name, issue.Index, token)
 	req := NewRequestWithValues(t, "POST", urlStr, map[string]string{
@@ -118,7 +118,7 @@ func TestAPIGetComment(t *testing.T) {
 	repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: comment.Issue.RepoID})
 	repoOwner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
 
-	token := getUserToken(t, repoOwner.Name, auth_model.AccessTokenScopeRepo)
+	token := getUserToken(t, repoOwner.Name, auth_model.AccessTokenScopeReadIssue)
 	req := NewRequestf(t, "GET", "/api/v1/repos/%s/%s/issues/comments/%d", repoOwner.Name, repo.Name, comment.ID)
 	MakeRequest(t, req, http.StatusOK)
 	req = NewRequestf(t, "GET", "/api/v1/repos/%s/%s/issues/comments/%d?token=%s", repoOwner.Name, repo.Name, comment.ID, token)
@@ -146,7 +146,7 @@ func TestAPIEditComment(t *testing.T) {
 	repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: issue.RepoID})
 	repoOwner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
 
-	token := getUserToken(t, repoOwner.Name, auth_model.AccessTokenScopeRepo)
+	token := getUserToken(t, repoOwner.Name, auth_model.AccessTokenScopeWriteIssue)
 	urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/issues/comments/%d?token=%s",
 		repoOwner.Name, repo.Name, comment.ID, token)
 	req := NewRequestWithValues(t, "PATCH", urlStr, map[string]string{
@@ -170,7 +170,7 @@ func TestAPIDeleteComment(t *testing.T) {
 	repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: issue.RepoID})
 	repoOwner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
 
-	token := getUserToken(t, repoOwner.Name, auth_model.AccessTokenScopeRepo)
+	token := getUserToken(t, repoOwner.Name, auth_model.AccessTokenScopeWriteIssue)
 	req := NewRequestf(t, "DELETE", "/api/v1/repos/%s/%s/issues/comments/%d?token=%s",
 		repoOwner.Name, repo.Name, comment.ID, token)
 	MakeRequest(t, req, http.StatusNoContent)
diff --git a/tests/integration/api_gpg_keys_test.go b/tests/integration/api_gpg_keys_test.go
index f66961786f..a4545bd0bb 100644
--- a/tests/integration/api_gpg_keys_test.go
+++ b/tests/integration/api_gpg_keys_test.go
@@ -21,8 +21,8 @@ type makeRequestFunc func(testing.TB, *http.Request, int) *httptest.ResponseReco
 func TestGPGKeys(t *testing.T) {
 	defer tests.PrepareTestEnv(t)()
 	session := loginUser(t, "user2")
-	token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeRepo)
-	tokenWithGPGKeyScope := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeAdminGPGKey, auth_model.AccessTokenScopeRepo)
+	token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository)
+	tokenWithGPGKeyScope := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser)
 
 	tt := []struct {
 		name        string
@@ -36,7 +36,7 @@ func TestGPGKeys(t *testing.T) {
 		},
 		{
 			name: "LoggedAsUser2", makeRequest: session.MakeRequest, token: token,
-			results: []int{http.StatusForbidden, http.StatusOK, http.StatusForbidden, http.StatusForbidden, http.StatusForbidden, http.StatusForbidden, http.StatusForbidden, http.StatusForbidden, http.StatusForbidden},
+			results: []int{http.StatusForbidden, http.StatusForbidden, http.StatusForbidden, http.StatusForbidden, http.StatusForbidden, http.StatusForbidden, http.StatusForbidden, http.StatusForbidden, http.StatusForbidden},
 		},
 		{
 			name: "LoggedAsUser2WithScope", makeRequest: session.MakeRequest, token: tokenWithGPGKeyScope,
diff --git a/tests/integration/api_httpsig_test.go b/tests/integration/api_httpsig_test.go
index 57f83490dc..4520364527 100644
--- a/tests/integration/api_httpsig_test.go
+++ b/tests/integration/api_httpsig_test.go
@@ -53,7 +53,7 @@ func TestHTTPSigPubKey(t *testing.T) {
 	// Add our public key to user1
 	defer tests.PrepareTestEnv(t)()
 	session := loginUser(t, "user1")
-	token := url.QueryEscape(getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeAdminPublicKey, auth_model.AccessTokenScopeSudo))
+	token := url.QueryEscape(getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteUser))
 	keysURL := fmt.Sprintf("/api/v1/user/keys?token=%s", token)
 	keyType := "ssh-rsa"
 	keyContent := "AAAAB3NzaC1yc2EAAAADAQABAAABAQCqOZB5vkRvXFXups1/0StDRdG8plbNSwsWEnNnP4Bvurxa0+z3W9B8GLKnDiLw5MbpbMNyBlpXw13GfuIeciy10DWTz0xUbiy3J3KabCaT36asIw2y7k6Z0jL0UBnrVENwq5/lUbZYqSZ4rRU744wkhh8TULpzM14npQCZwg6aEbG+MwjzddQ72fR+3BPBrKn5dTmmu8rH99O+U+Nuto81Tg7PA+NUupcHOmhdiEGq49plgVFXK98Vks5tiybL4GuzFyWgyX73Dg/QBMn2eMHt1EMv5Gs3i6GFhKKGo4rjDi9qI6PX5oDR4LTNe6cR8td8YhVD8WFZwLLl/vaYyIqd"
@@ -69,7 +69,8 @@ func TestHTTPSigPubKey(t *testing.T) {
 	keyID := ssh.FingerprintSHA256(sshSigner.PublicKey())
 
 	// create the request
-	req = NewRequest(t, "GET", "/api/v1/admin/users")
+	token = getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeReadAdmin)
+	req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/admin/users?token=%s", token))
 
 	signer, _, err := httpsig.NewSSHSigner(sshSigner, httpsig.DigestSha512, []string{httpsig.RequestTarget, "(created)", "(expires)"}, httpsig.Signature, 10)
 	if err != nil {
diff --git a/tests/integration/api_issue_attachment_test.go b/tests/integration/api_issue_attachment_test.go
index b4d6dab42a..3b43ba2c41 100644
--- a/tests/integration/api_issue_attachment_test.go
+++ b/tests/integration/api_issue_attachment_test.go
@@ -32,7 +32,7 @@ func TestAPIGetIssueAttachment(t *testing.T) {
 	repoOwner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
 
 	session := loginUser(t, repoOwner.Name)
-	token := getTokenForLoggedInUser(t, session)
+	token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeReadIssue)
 	urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/issues/%d/assets/%d?token=%s",
 		repoOwner.Name, repo.Name, issue.Index, attachment.ID, token)
 
@@ -53,7 +53,7 @@ func TestAPIListIssueAttachments(t *testing.T) {
 	repoOwner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
 
 	session := loginUser(t, repoOwner.Name)
-	token := getTokenForLoggedInUser(t, session)
+	token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeReadIssue)
 	urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/issues/%d/assets?token=%s",
 		repoOwner.Name, repo.Name, issue.Index, token)
 
@@ -73,7 +73,7 @@ func TestAPICreateIssueAttachment(t *testing.T) {
 	repoOwner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
 
 	session := loginUser(t, repoOwner.Name)
-	token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeRepo)
+	token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteIssue)
 	urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/issues/%d/assets?token=%s",
 		repoOwner.Name, repo.Name, issue.Index, token)
 
@@ -111,7 +111,7 @@ func TestAPIEditIssueAttachment(t *testing.T) {
 	repoOwner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
 
 	session := loginUser(t, repoOwner.Name)
-	token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeRepo)
+	token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteIssue)
 	urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/issues/%d/assets/%d?token=%s",
 		repoOwner.Name, repo.Name, issue.Index, attachment.ID, token)
 	req := NewRequestWithValues(t, "PATCH", urlStr, map[string]string{
@@ -133,7 +133,7 @@ func TestAPIDeleteIssueAttachment(t *testing.T) {
 	repoOwner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
 
 	session := loginUser(t, repoOwner.Name)
-	token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeRepo)
+	token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteIssue)
 	urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/issues/%d/assets/%d?token=%s",
 		repoOwner.Name, repo.Name, issue.Index, attachment.ID, token)
 
diff --git a/tests/integration/api_issue_label_test.go b/tests/integration/api_issue_label_test.go
index 1824015983..d2d8af102b 100644
--- a/tests/integration/api_issue_label_test.go
+++ b/tests/integration/api_issue_label_test.go
@@ -25,7 +25,7 @@ func TestAPIModifyLabels(t *testing.T) {
 	repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 2})
 	owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
 	session := loginUser(t, owner.Name)
-	token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeRepo)
+	token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteIssue)
 	urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/labels?token=%s", owner.Name, repo.Name, token)
 
 	// CreateLabel
@@ -97,7 +97,7 @@ func TestAPIAddIssueLabels(t *testing.T) {
 	owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
 
 	session := loginUser(t, owner.Name)
-	token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeRepo)
+	token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteIssue)
 	urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/issues/%d/labels?token=%s",
 		repo.OwnerName, repo.Name, issue.Index, token)
 	req := NewRequestWithJSON(t, "POST", urlStr, &api.IssueLabelsOption{
@@ -120,7 +120,7 @@ func TestAPIReplaceIssueLabels(t *testing.T) {
 	owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
 
 	session := loginUser(t, owner.Name)
-	token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeRepo)
+	token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteIssue)
 	urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/issues/%d/labels?token=%s",
 		owner.Name, repo.Name, issue.Index, token)
 	req := NewRequestWithJSON(t, "PUT", urlStr, &api.IssueLabelsOption{
@@ -144,7 +144,7 @@ func TestAPIModifyOrgLabels(t *testing.T) {
 	owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
 	user := "user1"
 	session := loginUser(t, user)
-	token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeRepo, auth_model.AccessTokenScopeAdminOrg)
+	token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteOrganization)
 	urlStr := fmt.Sprintf("/api/v1/orgs/%s/labels?token=%s", owner.Name, token)
 
 	// CreateLabel
diff --git a/tests/integration/api_issue_milestone_test.go b/tests/integration/api_issue_milestone_test.go
index cbce795bc9..3bd763f4b5 100644
--- a/tests/integration/api_issue_milestone_test.go
+++ b/tests/integration/api_issue_milestone_test.go
@@ -29,7 +29,7 @@ func TestAPIIssuesMilestone(t *testing.T) {
 	assert.Equal(t, structs.StateOpen, milestone.State())
 
 	session := loginUser(t, owner.Name)
-	token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeRepo)
+	token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteIssue)
 
 	// update values of issue
 	milestoneState := "closed"
diff --git a/tests/integration/api_issue_pin_test.go b/tests/integration/api_issue_pin_test.go
index 65be1d74f2..5a9efc058b 100644
--- a/tests/integration/api_issue_pin_test.go
+++ b/tests/integration/api_issue_pin_test.go
@@ -29,7 +29,7 @@ func TestAPIPinIssue(t *testing.T) {
 	owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
 
 	session := loginUser(t, owner.Name)
-	token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeRepo)
+	token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteIssue)
 
 	// Pin the Issue
 	urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/issues/%d/pin?token=%s",
@@ -56,7 +56,7 @@ func TestAPIUnpinIssue(t *testing.T) {
 	owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
 
 	session := loginUser(t, owner.Name)
-	token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeRepo)
+	token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteIssue)
 
 	// Pin the Issue
 	urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/issues/%d/pin?token=%s",
@@ -97,7 +97,7 @@ func TestAPIMoveIssuePin(t *testing.T) {
 	owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
 
 	session := loginUser(t, owner.Name)
-	token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeRepo)
+	token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteIssue)
 
 	// Pin the first Issue
 	urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/issues/%d/pin?token=%s",
@@ -152,7 +152,7 @@ func TestAPIListPinnedIssues(t *testing.T) {
 	owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
 
 	session := loginUser(t, owner.Name)
-	token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeRepo)
+	token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteIssue)
 
 	// Pin the Issue
 	urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/issues/%d/pin?token=%s",
diff --git a/tests/integration/api_issue_reaction_test.go b/tests/integration/api_issue_reaction_test.go
index 42793c66cf..7d3ee2d154 100644
--- a/tests/integration/api_issue_reaction_test.go
+++ b/tests/integration/api_issue_reaction_test.go
@@ -29,7 +29,7 @@ func TestAPIIssuesReactions(t *testing.T) {
 	owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: issue.Repo.OwnerID})
 
 	session := loginUser(t, owner.Name)
-	token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeRepo)
+	token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteIssue)
 
 	user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
 	urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/issues/%d/reactions?token=%s",
@@ -88,7 +88,7 @@ func TestAPICommentReactions(t *testing.T) {
 	owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: issue.Repo.OwnerID})
 
 	session := loginUser(t, owner.Name)
-	token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeRepo)
+	token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteIssue)
 
 	user1 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
 	user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
diff --git a/tests/integration/api_issue_stopwatch_test.go b/tests/integration/api_issue_stopwatch_test.go
index a8a832414d..09d404ce4e 100644
--- a/tests/integration/api_issue_stopwatch_test.go
+++ b/tests/integration/api_issue_stopwatch_test.go
@@ -26,7 +26,7 @@ func TestAPIListStopWatches(t *testing.T) {
 	owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
 
 	session := loginUser(t, owner.Name)
-	token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeRepo)
+	token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeReadRepository, auth_model.AccessTokenScopeReadUser)
 	req := NewRequestf(t, "GET", "/api/v1/user/stopwatches?token=%s", token)
 	resp := MakeRequest(t, req, http.StatusOK)
 	var apiWatches []*api.StopWatch
@@ -52,7 +52,7 @@ func TestAPIStopStopWatches(t *testing.T) {
 	user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
 
 	session := loginUser(t, user.Name)
-	token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeRepo)
+	token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteIssue)
 
 	req := NewRequestf(t, "POST", "/api/v1/repos/%s/%s/issues/%d/stopwatch/stop?token=%s", owner.Name, issue.Repo.Name, issue.Index, token)
 	MakeRequest(t, req, http.StatusCreated)
@@ -68,7 +68,7 @@ func TestAPICancelStopWatches(t *testing.T) {
 	user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
 
 	session := loginUser(t, user.Name)
-	token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeRepo)
+	token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteIssue)
 
 	req := NewRequestf(t, "DELETE", "/api/v1/repos/%s/%s/issues/%d/stopwatch/delete?token=%s", owner.Name, issue.Repo.Name, issue.Index, token)
 	MakeRequest(t, req, http.StatusNoContent)
@@ -84,7 +84,7 @@ func TestAPIStartStopWatches(t *testing.T) {
 	user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
 
 	session := loginUser(t, user.Name)
-	token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeRepo)
+	token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteIssue)
 
 	req := NewRequestf(t, "POST", "/api/v1/repos/%s/%s/issues/%d/stopwatch/start?token=%s", owner.Name, issue.Repo.Name, issue.Index, token)
 	MakeRequest(t, req, http.StatusCreated)
diff --git a/tests/integration/api_issue_subscription_test.go b/tests/integration/api_issue_subscription_test.go
index 473e720754..09c83b63b1 100644
--- a/tests/integration/api_issue_subscription_test.go
+++ b/tests/integration/api_issue_subscription_test.go
@@ -31,7 +31,7 @@ func TestAPIIssueSubscriptions(t *testing.T) {
 	owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: issue1.PosterID})
 
 	session := loginUser(t, owner.Name)
-	token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeRepo)
+	token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteIssue)
 
 	testSubscription := func(issue *issues_model.Issue, isWatching bool) {
 		issueRepo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: issue.RepoID})
diff --git a/tests/integration/api_issue_test.go b/tests/integration/api_issue_test.go
index 4344c15ea4..324f5ddbee 100644
--- a/tests/integration/api_issue_test.go
+++ b/tests/integration/api_issue_test.go
@@ -30,7 +30,7 @@ func TestAPIListIssues(t *testing.T) {
 	owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
 
 	session := loginUser(t, owner.Name)
-	token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeRepo)
+	token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeReadIssue)
 	link, _ := url.Parse(fmt.Sprintf("/api/v1/repos/%s/%s/issues", owner.Name, repo.Name))
 
 	link.RawQuery = url.Values{"token": {token}, "state": {"all"}}.Encode()
@@ -81,7 +81,7 @@ func TestAPICreateIssue(t *testing.T) {
 	owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repoBefore.OwnerID})
 
 	session := loginUser(t, owner.Name)
-	token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeRepo)
+	token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteIssue)
 	urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/issues?state=all&token=%s", owner.Name, repoBefore.Name, token)
 	req := NewRequestWithJSON(t, "POST", urlStr, &api.CreateIssueOption{
 		Body:     body,
@@ -117,7 +117,7 @@ func TestAPIEditIssue(t *testing.T) {
 	assert.Equal(t, api.StateOpen, issueBefore.State())
 
 	session := loginUser(t, owner.Name)
-	token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeRepo)
+	token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteIssue)
 
 	// update values of issue
 	issueState := "closed"
@@ -171,7 +171,7 @@ func TestAPIEditIssue(t *testing.T) {
 func TestAPISearchIssues(t *testing.T) {
 	defer tests.PrepareTestEnv(t)()
 
-	token := getUserToken(t, "user2")
+	token := getUserToken(t, "user2", auth_model.AccessTokenScopeReadIssue)
 
 	// as this API was used in the frontend, it uses UI page size
 	expectedIssueCount := 16 // from the fixtures
@@ -180,7 +180,7 @@ func TestAPISearchIssues(t *testing.T) {
 	}
 
 	link, _ := url.Parse("/api/v1/repos/issues/search")
-	query := url.Values{"token": {getUserToken(t, "user1")}}
+	query := url.Values{"token": {getUserToken(t, "user1", auth_model.AccessTokenScopeReadIssue)}}
 	var apiIssues []*api.Issue
 
 	link.RawQuery = query.Encode()
@@ -278,7 +278,7 @@ func TestAPISearchIssuesWithLabels(t *testing.T) {
 	}
 
 	link, _ := url.Parse("/api/v1/repos/issues/search")
-	query := url.Values{"token": {getUserToken(t, "user1")}}
+	query := url.Values{"token": {getUserToken(t, "user1", auth_model.AccessTokenScopeReadIssue)}}
 	var apiIssues []*api.Issue
 
 	link.RawQuery = query.Encode()
diff --git a/tests/integration/api_issue_tracked_time_test.go b/tests/integration/api_issue_tracked_time_test.go
index 7d9c785474..d3e45456a6 100644
--- a/tests/integration/api_issue_tracked_time_test.go
+++ b/tests/integration/api_issue_tracked_time_test.go
@@ -28,7 +28,7 @@ func TestAPIGetTrackedTimes(t *testing.T) {
 	assert.NoError(t, issue2.LoadRepo(db.DefaultContext))
 
 	session := loginUser(t, user2.Name)
-	token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeRepo)
+	token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeReadIssue)
 
 	req := NewRequestf(t, "GET", "/api/v1/repos/%s/%s/issues/%d/times?token=%s", user2.Name, issue2.Repo.Name, issue2.Index, token)
 	resp := MakeRequest(t, req, http.StatusOK)
@@ -71,7 +71,7 @@ func TestAPIDeleteTrackedTime(t *testing.T) {
 	user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
 
 	session := loginUser(t, user2.Name)
-	token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeRepo)
+	token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteIssue)
 
 	// Deletion not allowed
 	req := NewRequestf(t, "DELETE", "/api/v1/repos/%s/%s/issues/%d/times/%d?token=%s", user2.Name, issue2.Repo.Name, issue2.Index, time6.ID, token)
@@ -106,7 +106,7 @@ func TestAPIAddTrackedTimes(t *testing.T) {
 	admin := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
 
 	session := loginUser(t, admin.Name)
-	token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeRepo)
+	token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteIssue)
 
 	urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/issues/%d/times?token=%s", user2.Name, issue2.Repo.Name, issue2.Index, token)
 
diff --git a/tests/integration/api_keys_test.go b/tests/integration/api_keys_test.go
index dc25cbfc1a..238c3cb823 100644
--- a/tests/integration/api_keys_test.go
+++ b/tests/integration/api_keys_test.go
@@ -54,7 +54,7 @@ func TestCreateReadOnlyDeployKey(t *testing.T) {
 	repoOwner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
 
 	session := loginUser(t, repoOwner.Name)
-	token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeRepo)
+	token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository)
 	keysURL := fmt.Sprintf("/api/v1/repos/%s/%s/keys?token=%s", repoOwner.Name, repo.Name, token)
 	rawKeyBody := api.CreateKeyOption{
 		Title:    "read-only",
@@ -80,7 +80,7 @@ func TestCreateReadWriteDeployKey(t *testing.T) {
 	repoOwner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
 
 	session := loginUser(t, repoOwner.Name)
-	token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeRepo)
+	token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository)
 	keysURL := fmt.Sprintf("/api/v1/repos/%s/%s/keys?token=%s", repoOwner.Name, repo.Name, token)
 	rawKeyBody := api.CreateKeyOption{
 		Title: "read-write",
@@ -104,7 +104,7 @@ func TestCreateUserKey(t *testing.T) {
 	user := unittest.AssertExistsAndLoadBean(t, &user_model.User{Name: "user1"})
 
 	session := loginUser(t, "user1")
-	token := url.QueryEscape(getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeAdminPublicKey))
+	token := url.QueryEscape(getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteUser))
 	keysURL := fmt.Sprintf("/api/v1/user/keys?token=%s", token)
 	keyType := "ssh-rsa"
 	keyContent := "AAAAB3NzaC1yc2EAAAADAQABAAABgQC4cn+iXnA4KvcQYSV88vGn0Yi91vG47t1P7okprVmhNTkipNRIHWr6WdCO4VDr/cvsRkuVJAsLO2enwjGWWueOO6BodiBgyAOZ/5t5nJNMCNuLGT5UIo/RI1b0WRQwxEZTRjt6mFNw6lH14wRd8ulsr9toSWBPMOGWoYs1PDeDL0JuTjL+tr1SZi/EyxCngpYszKdXllJEHyI79KQgeD0Vt3pTrkbNVTOEcCNqZePSVmUH8X8Vhugz3bnE0/iE9Pb5fkWO9c4AnM1FgI/8Bvp27Fw2ShryIXuR6kKvUqhVMTuOSDHwu6A8jLE5Owt3GAYugDpDYuwTVNGrHLXKpPzrGGPE/jPmaLCMZcsdkec95dYeU3zKODEm8UQZFhmJmDeWVJ36nGrGZHL4J5aTTaeFUJmmXDaJYiJ+K2/ioKgXqnXvltu0A9R8/LGy4nrTJRr4JMLuJFoUXvGm1gXQ70w2LSpk6yl71RNC0hCtsBe8BP8IhYCM0EP5jh7eCMQZNvM="
@@ -168,7 +168,7 @@ func TestCreateUserKey(t *testing.T) {
 
 	// Now login as user 2
 	session2 := loginUser(t, "user2")
-	token2 := url.QueryEscape(getTokenForLoggedInUser(t, session2, auth_model.AccessTokenScopeAdminPublicKey))
+	token2 := url.QueryEscape(getTokenForLoggedInUser(t, session2, auth_model.AccessTokenScopeWriteUser))
 
 	// Should find key even though not ours, but we shouldn't know whose it is
 	fingerprintURL = fmt.Sprintf("/api/v1/user/keys?token=%s&fingerprint=%s", token2, newPublicKey.Fingerprint)
diff --git a/tests/integration/api_notification_test.go b/tests/integration/api_notification_test.go
index 0ff13704cf..52d6e6d84a 100644
--- a/tests/integration/api_notification_test.go
+++ b/tests/integration/api_notification_test.go
@@ -28,7 +28,7 @@ func TestAPINotification(t *testing.T) {
 	thread5 := unittest.AssertExistsAndLoadBean(t, &activities_model.Notification{ID: 5})
 	assert.NoError(t, thread5.LoadAttributes(db.DefaultContext))
 	session := loginUser(t, user2.Name)
-	token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeNotification)
+	token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteNotification, auth_model.AccessTokenScopeWriteRepository)
 
 	// -- GET /notifications --
 	// test filter
@@ -146,7 +146,7 @@ func TestAPINotificationPUT(t *testing.T) {
 	thread5 := unittest.AssertExistsAndLoadBean(t, &activities_model.Notification{ID: 5})
 	assert.NoError(t, thread5.LoadAttributes(db.DefaultContext))
 	session := loginUser(t, user2.Name)
-	token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeNotification)
+	token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteNotification)
 
 	// Check notifications are as expected
 	req := NewRequest(t, "GET", fmt.Sprintf("/api/v1/notifications?all=true&token=%s", token))
diff --git a/tests/integration/api_oauth2_apps_test.go b/tests/integration/api_oauth2_apps_test.go
index c320efb391..72cdba2ea2 100644
--- a/tests/integration/api_oauth2_apps_test.go
+++ b/tests/integration/api_oauth2_apps_test.go
@@ -55,7 +55,7 @@ func testAPICreateOAuth2Application(t *testing.T) {
 func testAPIListOAuth2Applications(t *testing.T) {
 	user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
 	session := loginUser(t, user.Name)
-	token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeReadApplication)
+	token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeReadUser)
 
 	existApp := unittest.AssertExistsAndLoadBean(t, &auth_model.OAuth2Application{
 		UID:  user.ID,
@@ -86,7 +86,7 @@ func testAPIListOAuth2Applications(t *testing.T) {
 func testAPIDeleteOAuth2Application(t *testing.T) {
 	user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
 	session := loginUser(t, user.Name)
-	token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteApplication)
+	token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteUser)
 
 	oldApp := unittest.AssertExistsAndLoadBean(t, &auth_model.OAuth2Application{
 		UID:  user.ID,
@@ -107,7 +107,7 @@ func testAPIDeleteOAuth2Application(t *testing.T) {
 func testAPIGetOAuth2Application(t *testing.T) {
 	user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
 	session := loginUser(t, user.Name)
-	token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeReadApplication)
+	token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeReadUser)
 
 	existApp := unittest.AssertExistsAndLoadBean(t, &auth_model.OAuth2Application{
 		UID:  user.ID,
diff --git a/tests/integration/api_org_test.go b/tests/integration/api_org_test.go
index 4b79b32c59..edbf576b9e 100644
--- a/tests/integration/api_org_test.go
+++ b/tests/integration/api_org_test.go
@@ -26,7 +26,7 @@ import (
 
 func TestAPIOrgCreate(t *testing.T) {
 	onGiteaRun(t, func(*testing.T, *url.URL) {
-		token := getUserToken(t, "user1", auth_model.AccessTokenScopeWriteOrg)
+		token := getUserToken(t, "user1", auth_model.AccessTokenScopeWriteOrganization)
 
 		org := api.CreateOrgOption{
 			UserName:    "user1_org",
@@ -100,7 +100,7 @@ func TestAPIOrgEdit(t *testing.T) {
 	onGiteaRun(t, func(*testing.T, *url.URL) {
 		session := loginUser(t, "user1")
 
-		token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteOrg)
+		token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteOrganization)
 		org := api.EditOrgOption{
 			FullName:    "User3 organization new full name",
 			Description: "A new description",
@@ -127,7 +127,7 @@ func TestAPIOrgEditBadVisibility(t *testing.T) {
 	onGiteaRun(t, func(*testing.T, *url.URL) {
 		session := loginUser(t, "user1")
 
-		token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteOrg)
+		token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteOrganization)
 		org := api.EditOrgOption{
 			FullName:    "User3 organization new full name",
 			Description: "A new description",
@@ -162,7 +162,7 @@ func TestAPIOrgDeny(t *testing.T) {
 func TestAPIGetAll(t *testing.T) {
 	defer tests.PrepareTestEnv(t)()
 
-	token := getUserToken(t, "user1", auth_model.AccessTokenScopeReadOrg)
+	token := getUserToken(t, "user1", auth_model.AccessTokenScopeReadOrganization)
 
 	// accessing with a token will return all orgs
 	req := NewRequestf(t, "GET", "/api/v1/orgs?token=%s", token)
@@ -186,7 +186,7 @@ func TestAPIGetAll(t *testing.T) {
 
 func TestAPIOrgSearchEmptyTeam(t *testing.T) {
 	onGiteaRun(t, func(*testing.T, *url.URL) {
-		token := getUserToken(t, "user1", auth_model.AccessTokenScopeAdminOrg)
+		token := getUserToken(t, "user1", auth_model.AccessTokenScopeWriteOrganization)
 		orgName := "org_with_empty_team"
 
 		// create org
diff --git a/tests/integration/api_packages_npm_test.go b/tests/integration/api_packages_npm_test.go
index 78389b5740..433b183c29 100644
--- a/tests/integration/api_packages_npm_test.go
+++ b/tests/integration/api_packages_npm_test.go
@@ -28,7 +28,7 @@ func TestPackageNpm(t *testing.T) {
 
 	user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
 
-	token := fmt.Sprintf("Bearer %s", getTokenForLoggedInUser(t, loginUser(t, user.Name), auth_model.AccessTokenScopePackage))
+	token := fmt.Sprintf("Bearer %s", getTokenForLoggedInUser(t, loginUser(t, user.Name), auth_model.AccessTokenScopeWritePackage))
 
 	packageName := "@scope/test-package"
 	packageVersion := "1.0.1-pre"
diff --git a/tests/integration/api_packages_nuget_test.go b/tests/integration/api_packages_nuget_test.go
index 2240d2a5d4..a6c4090f0e 100644
--- a/tests/integration/api_packages_nuget_test.go
+++ b/tests/integration/api_packages_nuget_test.go
@@ -75,7 +75,7 @@ func TestPackageNuGet(t *testing.T) {
 	}
 
 	user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
-	token := getUserToken(t, user.Name, auth_model.AccessTokenScopePackage)
+	token := getUserToken(t, user.Name, auth_model.AccessTokenScopeWritePackage)
 
 	packageName := "test.package"
 	packageVersion := "1.0.3"
diff --git a/tests/integration/api_packages_pub_test.go b/tests/integration/api_packages_pub_test.go
index 5c1cc6052f..df0c7aa21b 100644
--- a/tests/integration/api_packages_pub_test.go
+++ b/tests/integration/api_packages_pub_test.go
@@ -31,7 +31,7 @@ func TestPackagePub(t *testing.T) {
 
 	user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
 
-	token := "Bearer " + getUserToken(t, user.Name, auth_model.AccessTokenScopePackage)
+	token := "Bearer " + getUserToken(t, user.Name, auth_model.AccessTokenScopeWritePackage)
 
 	packageName := "test_package"
 	packageVersion := "1.0.1"
diff --git a/tests/integration/api_packages_test.go b/tests/integration/api_packages_test.go
index 74a7e3c795..84733f683b 100644
--- a/tests/integration/api_packages_test.go
+++ b/tests/integration/api_packages_test.go
@@ -34,7 +34,7 @@ func TestPackageAPI(t *testing.T) {
 	user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4})
 	session := loginUser(t, user.Name)
 	tokenReadPackage := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeReadPackage)
-	tokenDeletePackage := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeDeletePackage)
+	tokenDeletePackage := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWritePackage)
 
 	packageName := "test-package"
 	packageVersion := "1.0.3"
diff --git a/tests/integration/api_packages_vagrant_test.go b/tests/integration/api_packages_vagrant_test.go
index b28bfca6f0..cbfc362f32 100644
--- a/tests/integration/api_packages_vagrant_test.go
+++ b/tests/integration/api_packages_vagrant_test.go
@@ -28,7 +28,7 @@ func TestPackageVagrant(t *testing.T) {
 	defer tests.PrepareTestEnv(t)()
 	user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
 
-	token := "Bearer " + getUserToken(t, user.Name, auth_model.AccessTokenScopePackage)
+	token := "Bearer " + getUserToken(t, user.Name, auth_model.AccessTokenScopeWritePackage)
 
 	packageName := "test_package"
 	packageVersion := "1.0.1"
diff --git a/tests/integration/api_pull_review_test.go b/tests/integration/api_pull_review_test.go
index e0abf34f5e..4718f115cb 100644
--- a/tests/integration/api_pull_review_test.go
+++ b/tests/integration/api_pull_review_test.go
@@ -28,7 +28,7 @@ func TestAPIPullReview(t *testing.T) {
 
 	// test ListPullReviews
 	session := loginUser(t, "user2")
-	token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeRepo)
+	token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository)
 	req := NewRequestf(t, http.MethodGet, "/api/v1/repos/%s/%s/pulls/%d/reviews?token=%s", repo.OwnerName, repo.Name, pullIssue.Index, token)
 	resp := MakeRequest(t, req, http.StatusOK)
 
@@ -231,7 +231,7 @@ func TestAPIPullReviewRequest(t *testing.T) {
 
 	// Test add Review Request
 	session := loginUser(t, "user2")
-	token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeRepo)
+	token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository)
 	req := NewRequestWithJSON(t, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/%s/pulls/%d/requested_reviewers?token=%s", repo.OwnerName, repo.Name, pullIssue.Index, token), &api.PullReviewRequestOptions{
 		Reviewers: []string{"user4@example.com", "user8"},
 	})
@@ -251,7 +251,7 @@ func TestAPIPullReviewRequest(t *testing.T) {
 
 	// Test Remove Review Request
 	session2 := loginUser(t, "user4")
-	token2 := getTokenForLoggedInUser(t, session2, auth_model.AccessTokenScopeRepo)
+	token2 := getTokenForLoggedInUser(t, session2, auth_model.AccessTokenScopeWriteRepository)
 
 	req = NewRequestWithJSON(t, http.MethodDelete, fmt.Sprintf("/api/v1/repos/%s/%s/pulls/%d/requested_reviewers?token=%s", repo.OwnerName, repo.Name, pullIssue.Index, token2), &api.PullReviewRequestOptions{
 		Reviewers: []string{"user4"},
diff --git a/tests/integration/api_pull_test.go b/tests/integration/api_pull_test.go
index cf675f9740..9d590630e4 100644
--- a/tests/integration/api_pull_test.go
+++ b/tests/integration/api_pull_test.go
@@ -29,7 +29,7 @@ func TestAPIViewPulls(t *testing.T) {
 	repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
 	owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
 
-	ctx := NewAPITestContext(t, "user2", repo.Name, auth_model.AccessTokenScopeRepo)
+	ctx := NewAPITestContext(t, "user2", repo.Name, auth_model.AccessTokenScopeReadRepository)
 
 	req := NewRequestf(t, "GET", "/api/v1/repos/%s/%s/pulls?state=all&token="+ctx.Token, owner.Name, repo.Name)
 	resp := ctx.Session.MakeRequest(t, req, http.StatusOK)
@@ -75,7 +75,7 @@ func TestAPIMergePullWIP(t *testing.T) {
 	assert.Contains(t, pr.Issue.Title, setting.Repository.PullRequest.WorkInProgressPrefixes[0])
 
 	session := loginUser(t, owner.Name)
-	token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeRepo)
+	token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository)
 	req := NewRequestWithJSON(t, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/%s/pulls/%d/merge?token=%s", owner.Name, repo.Name, pr.Index, token), &forms.MergePullRequestForm{
 		MergeMessageField: pr.Issue.Title,
 		Do:                string(repo_model.MergeStyleMerge),
@@ -94,7 +94,7 @@ func TestAPICreatePullSuccess(t *testing.T) {
 	owner11 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo11.OwnerID})
 
 	session := loginUser(t, owner11.Name)
-	token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeRepo)
+	token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository)
 	req := NewRequestWithJSON(t, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/%s/pulls?token=%s", owner10.Name, repo10.Name, token), &api.CreatePullRequestOption{
 		Head:  fmt.Sprintf("%s:master", owner11.Name),
 		Base:  "master",
@@ -114,7 +114,7 @@ func TestAPICreatePullWithFieldsSuccess(t *testing.T) {
 	owner11 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo11.OwnerID})
 
 	session := loginUser(t, owner11.Name)
-	token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeRepo)
+	token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository)
 
 	opts := &api.CreatePullRequestOption{
 		Head:      fmt.Sprintf("%s:master", owner11.Name),
@@ -151,7 +151,7 @@ func TestAPICreatePullWithFieldsFailure(t *testing.T) {
 	owner11 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo11.OwnerID})
 
 	session := loginUser(t, owner11.Name)
-	token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeRepo)
+	token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository)
 
 	opts := &api.CreatePullRequestOption{
 		Head: fmt.Sprintf("%s:master", owner11.Name),
@@ -181,7 +181,7 @@ func TestAPIEditPull(t *testing.T) {
 	owner10 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo10.OwnerID})
 
 	session := loginUser(t, owner10.Name)
-	token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeRepo)
+	token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository)
 	req := NewRequestWithJSON(t, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/%s/pulls?token=%s", owner10.Name, repo10.Name, token), &api.CreatePullRequestOption{
 		Head:  "develop",
 		Base:  "master",
diff --git a/tests/integration/api_releases_test.go b/tests/integration/api_releases_test.go
index aa5816ad02..7f43939083 100644
--- a/tests/integration/api_releases_test.go
+++ b/tests/integration/api_releases_test.go
@@ -25,7 +25,7 @@ func TestAPIListReleases(t *testing.T) {
 
 	repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
 	user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
-	token := getUserToken(t, user2.LowerName, auth_model.AccessTokenScopeRepo)
+	token := getUserToken(t, user2.LowerName, auth_model.AccessTokenScopeReadRepository)
 
 	link, _ := url.Parse(fmt.Sprintf("/api/v1/repos/%s/%s/releases", user2.Name, repo.Name))
 	link.RawQuery = url.Values{"token": {token}}.Encode()
@@ -101,7 +101,7 @@ func TestAPICreateAndUpdateRelease(t *testing.T) {
 	repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
 	owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
 	session := loginUser(t, owner.LowerName)
-	token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeRepo)
+	token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository)
 
 	gitRepo, err := git.OpenRepository(git.DefaultContext, repo.RepoPath())
 	assert.NoError(t, err)
@@ -153,7 +153,7 @@ func TestAPICreateReleaseToDefaultBranch(t *testing.T) {
 	repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
 	owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
 	session := loginUser(t, owner.LowerName)
-	token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeRepo)
+	token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository)
 
 	createNewReleaseUsingAPI(t, session, token, owner, repo, "v0.0.1", "", "v0.0.1", "test")
 }
@@ -164,7 +164,7 @@ func TestAPICreateReleaseToDefaultBranchOnExistingTag(t *testing.T) {
 	repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
 	owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
 	session := loginUser(t, owner.LowerName)
-	token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeRepo)
+	token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository)
 
 	gitRepo, err := git.OpenRepository(git.DefaultContext, repo.RepoPath())
 	assert.NoError(t, err)
@@ -232,7 +232,7 @@ func TestAPIDeleteReleaseByTagName(t *testing.T) {
 	repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
 	owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
 	session := loginUser(t, owner.LowerName)
-	token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeRepo)
+	token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository)
 
 	createNewReleaseUsingAPI(t, session, token, owner, repo, "release-tag", "", "Release Tag", "test")
 
diff --git a/tests/integration/api_repo_archive_test.go b/tests/integration/api_repo_archive_test.go
index 699d7a436c..5d1db1b09b 100644
--- a/tests/integration/api_repo_archive_test.go
+++ b/tests/integration/api_repo_archive_test.go
@@ -25,7 +25,7 @@ func TestAPIDownloadArchive(t *testing.T) {
 	repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
 	user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
 	session := loginUser(t, user2.LowerName)
-	token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeRepo)
+	token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeReadRepository)
 
 	link, _ := url.Parse(fmt.Sprintf("/api/v1/repos/%s/%s/archive/master.zip", user2.Name, repo.Name))
 	link.RawQuery = url.Values{"token": {token}}.Encode()
diff --git a/tests/integration/api_repo_collaborator_test.go b/tests/integration/api_repo_collaborator_test.go
index ed01538477..b7280a4f6c 100644
--- a/tests/integration/api_repo_collaborator_test.go
+++ b/tests/integration/api_repo_collaborator_test.go
@@ -28,7 +28,7 @@ func TestAPIRepoCollaboratorPermission(t *testing.T) {
 		user10 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 10})
 		user11 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 11})
 
-		testCtx := NewAPITestContext(t, repo2Owner.Name, repo2.Name, auth_model.AccessTokenScopeRepo)
+		testCtx := NewAPITestContext(t, repo2Owner.Name, repo2.Name, auth_model.AccessTokenScopeWriteRepository)
 
 		t.Run("RepoOwnerShouldBeOwner", func(t *testing.T) {
 			req := NewRequestf(t, "GET", "/api/v1/repos/%s/%s/collaborators/%s/permission?token=%s", repo2Owner.Name, repo2.Name, repo2Owner.Name, testCtx.Token)
@@ -85,7 +85,7 @@ func TestAPIRepoCollaboratorPermission(t *testing.T) {
 			t.Run("AddUserAsCollaboratorWithReadAccess", doAPIAddCollaborator(testCtx, user5.Name, perm.AccessModeRead))
 
 			_session := loginUser(t, user5.Name)
-			_testCtx := NewAPITestContext(t, user5.Name, repo2.Name, auth_model.AccessTokenScopeRepo)
+			_testCtx := NewAPITestContext(t, user5.Name, repo2.Name, auth_model.AccessTokenScopeReadRepository)
 
 			req := NewRequestf(t, "GET", "/api/v1/repos/%s/%s/collaborators/%s/permission?token=%s", repo2Owner.Name, repo2.Name, user5.Name, _testCtx.Token)
 			resp := _session.MakeRequest(t, req, http.StatusOK)
@@ -100,7 +100,7 @@ func TestAPIRepoCollaboratorPermission(t *testing.T) {
 			t.Run("AddUserAsCollaboratorWithReadAccess", doAPIAddCollaborator(testCtx, user5.Name, perm.AccessModeRead))
 
 			_session := loginUser(t, user5.Name)
-			_testCtx := NewAPITestContext(t, user5.Name, repo2.Name, auth_model.AccessTokenScopeRepo)
+			_testCtx := NewAPITestContext(t, user5.Name, repo2.Name, auth_model.AccessTokenScopeReadRepository)
 
 			req := NewRequestf(t, "GET", "/api/v1/repos/%s/%s/collaborators/%s/permission?token=%s", repo2Owner.Name, repo2.Name, user5.Name, _testCtx.Token)
 			resp := _session.MakeRequest(t, req, http.StatusOK)
@@ -116,7 +116,7 @@ func TestAPIRepoCollaboratorPermission(t *testing.T) {
 			t.Run("AddUserAsCollaboratorWithReadAccess", doAPIAddCollaborator(testCtx, user11.Name, perm.AccessModeRead))
 
 			_session := loginUser(t, user10.Name)
-			_testCtx := NewAPITestContext(t, user10.Name, repo2.Name, auth_model.AccessTokenScopeRepo)
+			_testCtx := NewAPITestContext(t, user10.Name, repo2.Name, auth_model.AccessTokenScopeReadRepository)
 
 			req := NewRequestf(t, "GET", "/api/v1/repos/%s/%s/collaborators/%s/permission?token=%s", repo2Owner.Name, repo2.Name, user11.Name, _testCtx.Token)
 			resp := _session.MakeRequest(t, req, http.StatusOK)
diff --git a/tests/integration/api_repo_edit_test.go b/tests/integration/api_repo_edit_test.go
index 30dc73ef1b..b7242fb316 100644
--- a/tests/integration/api_repo_edit_test.go
+++ b/tests/integration/api_repo_edit_test.go
@@ -147,10 +147,10 @@ func TestAPIRepoEdit(t *testing.T) {
 
 		// Get user2's token
 		session := loginUser(t, user2.Name)
-		token2 := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeRepo)
+		token2 := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository)
 		// Get user4's token
 		session = loginUser(t, user4.Name)
-		token4 := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeRepo)
+		token4 := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository)
 
 		// Test editing a repo1 which user2 owns, changing name and many properties
 		origRepoEditOption := getRepoEditOptionFromRepo(repo1)
diff --git a/tests/integration/api_repo_file_create_test.go b/tests/integration/api_repo_file_create_test.go
index a3a5422154..cbc164891b 100644
--- a/tests/integration/api_repo_file_create_test.go
+++ b/tests/integration/api_repo_file_create_test.go
@@ -151,10 +151,10 @@ func TestAPICreateFile(t *testing.T) {
 
 		// Get user2's token
 		session := loginUser(t, user2.Name)
-		token2 := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeRepo)
+		token2 := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser)
 		// Get user4's token
 		session = loginUser(t, user4.Name)
-		token4 := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeRepo)
+		token4 := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser)
 
 		// Test creating a file in repo1 which user2 owns, try both with branch and empty branch
 		for _, branch := range [...]string{
@@ -280,7 +280,7 @@ func TestAPICreateFile(t *testing.T) {
 		MakeRequest(t, req, http.StatusForbidden)
 
 		// Test creating a file in an empty repository
-		doAPICreateRepository(NewAPITestContext(t, "user2", "empty-repo", auth_model.AccessTokenScopeRepo), true)(t)
+		doAPICreateRepository(NewAPITestContext(t, "user2", "empty-repo", auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser), true)(t)
 		createFileOptions = getCreateFileOptions()
 		fileID++
 		treePath = fmt.Sprintf("new/file%d.txt", fileID)
diff --git a/tests/integration/api_repo_file_delete_test.go b/tests/integration/api_repo_file_delete_test.go
index ae28c97002..df9e801e2c 100644
--- a/tests/integration/api_repo_file_delete_test.go
+++ b/tests/integration/api_repo_file_delete_test.go
@@ -49,10 +49,10 @@ func TestAPIDeleteFile(t *testing.T) {
 
 		// Get user2's token
 		session := loginUser(t, user2.Name)
-		token2 := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeRepo)
+		token2 := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository)
 		// Get user4's token
 		session = loginUser(t, user4.Name)
-		token4 := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeRepo)
+		token4 := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository)
 
 		// Test deleting a file in repo1 which user2 owns, try both with branch and empty branch
 		for _, branch := range [...]string{
diff --git a/tests/integration/api_repo_file_get_test.go b/tests/integration/api_repo_file_get_test.go
index a6a1e63439..4649babad1 100644
--- a/tests/integration/api_repo_file_get_test.go
+++ b/tests/integration/api_repo_file_get_test.go
@@ -25,7 +25,7 @@ func TestAPIGetRawFileOrLFS(t *testing.T) {
 
 	// Test with LFS
 	onGiteaRun(t, func(t *testing.T, u *url.URL) {
-		httpContext := NewAPITestContext(t, "user2", "repo-lfs-test", auth_model.AccessTokenScopeRepo, auth_model.AccessTokenScopeDeleteRepo)
+		httpContext := NewAPITestContext(t, "user2", "repo-lfs-test", auth_model.AccessTokenScopeWriteRepository)
 		doAPICreateRepository(httpContext, false, func(t *testing.T, repository api.Repository) {
 			u.Path = httpContext.GitPath()
 			dstPath := t.TempDir()
diff --git a/tests/integration/api_repo_file_update_test.go b/tests/integration/api_repo_file_update_test.go
index 177d7282ca..5bcb531fc1 100644
--- a/tests/integration/api_repo_file_update_test.go
+++ b/tests/integration/api_repo_file_update_test.go
@@ -117,10 +117,10 @@ func TestAPIUpdateFile(t *testing.T) {
 
 		// Get user2's token
 		session := loginUser(t, user2.Name)
-		token2 := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeRepo)
+		token2 := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository)
 		// Get user4's token
 		session = loginUser(t, user4.Name)
-		token4 := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeRepo)
+		token4 := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository)
 
 		// Test updating a file in repo1 which user2 owns, try both with branch and empty branch
 		for _, branch := range [...]string{
diff --git a/tests/integration/api_repo_files_change_test.go b/tests/integration/api_repo_files_change_test.go
index 38187ec5b9..8599b12a7f 100644
--- a/tests/integration/api_repo_files_change_test.go
+++ b/tests/integration/api_repo_files_change_test.go
@@ -72,10 +72,10 @@ func TestAPIChangeFiles(t *testing.T) {
 
 		// Get user2's token
 		session := loginUser(t, user2.Name)
-		token2 := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeRepo)
+		token2 := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository)
 		// Get user4's token
 		session = loginUser(t, user4.Name)
-		token4 := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeRepo)
+		token4 := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository)
 
 		// Test changing files in repo1 which user2 owns, try both with branch and empty branch
 		for _, branch := range [...]string{
diff --git a/tests/integration/api_repo_get_contents_list_test.go b/tests/integration/api_repo_get_contents_list_test.go
index 2c7b44120c..f91305abef 100644
--- a/tests/integration/api_repo_get_contents_list_test.go
+++ b/tests/integration/api_repo_get_contents_list_test.go
@@ -9,6 +9,7 @@ import (
 	"path/filepath"
 	"testing"
 
+	auth_model "code.gitea.io/gitea/models/auth"
 	repo_model "code.gitea.io/gitea/models/repo"
 	"code.gitea.io/gitea/models/unittest"
 	user_model "code.gitea.io/gitea/models/user"
@@ -64,10 +65,10 @@ func testAPIGetContentsList(t *testing.T, u *url.URL) {
 
 	// Get user2's token
 	session := loginUser(t, user2.Name)
-	token2 := getTokenForLoggedInUser(t, session)
+	token2 := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeReadRepository)
 	// Get user4's token
 	session = loginUser(t, user4.Name)
-	token4 := getTokenForLoggedInUser(t, session)
+	token4 := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeReadRepository)
 
 	// Make a new branch in repo1
 	newBranch := "test_branch"
diff --git a/tests/integration/api_repo_get_contents_test.go b/tests/integration/api_repo_get_contents_test.go
index 8b193b03c9..be091cea4e 100644
--- a/tests/integration/api_repo_get_contents_test.go
+++ b/tests/integration/api_repo_get_contents_test.go
@@ -9,6 +9,7 @@ import (
 	"net/url"
 	"testing"
 
+	auth_model "code.gitea.io/gitea/models/auth"
 	repo_model "code.gitea.io/gitea/models/repo"
 	"code.gitea.io/gitea/models/unittest"
 	user_model "code.gitea.io/gitea/models/user"
@@ -66,10 +67,10 @@ func testAPIGetContents(t *testing.T, u *url.URL) {
 
 	// Get user2's token
 	session := loginUser(t, user2.Name)
-	token2 := getTokenForLoggedInUser(t, session)
+	token2 := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeReadRepository)
 	// Get user4's token
 	session = loginUser(t, user4.Name)
-	token4 := getTokenForLoggedInUser(t, session)
+	token4 := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeReadRepository)
 
 	// Make a new branch in repo1
 	newBranch := "test_branch"
diff --git a/tests/integration/api_repo_git_blobs_test.go b/tests/integration/api_repo_git_blobs_test.go
index 02652e5934..9a0bb6d0f2 100644
--- a/tests/integration/api_repo_git_blobs_test.go
+++ b/tests/integration/api_repo_git_blobs_test.go
@@ -7,6 +7,7 @@ import (
 	"net/http"
 	"testing"
 
+	auth_model "code.gitea.io/gitea/models/auth"
 	repo_model "code.gitea.io/gitea/models/repo"
 	"code.gitea.io/gitea/models/unittest"
 	user_model "code.gitea.io/gitea/models/user"
@@ -31,7 +32,7 @@ func TestAPIReposGitBlobs(t *testing.T) {
 
 	// Login as User2.
 	session := loginUser(t, user2.Name)
-	token := getTokenForLoggedInUser(t, session)
+	token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeReadRepository)
 
 	// Test a public repo that anyone can GET the blob of
 	req := NewRequestf(t, "GET", "/api/v1/repos/%s/%s/git/blobs/%s", user2.Name, repo1.Name, repo1ReadmeSHA)
diff --git a/tests/integration/api_repo_git_commits_test.go b/tests/integration/api_repo_git_commits_test.go
index 90048a5496..765055720d 100644
--- a/tests/integration/api_repo_git_commits_test.go
+++ b/tests/integration/api_repo_git_commits_test.go
@@ -7,6 +7,7 @@ import (
 	"net/http"
 	"testing"
 
+	auth_model "code.gitea.io/gitea/models/auth"
 	"code.gitea.io/gitea/models/unittest"
 	user_model "code.gitea.io/gitea/models/user"
 	api "code.gitea.io/gitea/modules/structs"
@@ -28,7 +29,7 @@ func TestAPIReposGitCommits(t *testing.T) {
 	user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
 	// Login as User2.
 	session := loginUser(t, user.Name)
-	token := getTokenForLoggedInUser(t, session)
+	token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeReadRepository)
 
 	// check invalid requests
 	req := NewRequestf(t, "GET", "/api/v1/repos/%s/repo1/git/commits/12345?token="+token, user.Name)
@@ -56,7 +57,7 @@ func TestAPIReposGitCommitList(t *testing.T) {
 	user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
 	// Login as User2.
 	session := loginUser(t, user.Name)
-	token := getTokenForLoggedInUser(t, session)
+	token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeReadRepository)
 
 	// Test getting commits (Page 1)
 	req := NewRequestf(t, "GET", "/api/v1/repos/%s/repo20/commits?token="+token+"&not=master&sha=remove-files-a", user.Name)
@@ -79,7 +80,7 @@ func TestAPIReposGitCommitListNotMaster(t *testing.T) {
 	user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
 	// Login as User2.
 	session := loginUser(t, user.Name)
-	token := getTokenForLoggedInUser(t, session)
+	token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeReadRepository)
 
 	// Test getting commits (Page 1)
 	req := NewRequestf(t, "GET", "/api/v1/repos/%s/repo16/commits?token="+token, user.Name)
@@ -104,7 +105,7 @@ func TestAPIReposGitCommitListPage2Empty(t *testing.T) {
 	user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
 	// Login as User2.
 	session := loginUser(t, user.Name)
-	token := getTokenForLoggedInUser(t, session)
+	token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeReadRepository)
 
 	// Test getting commits (Page=2)
 	req := NewRequestf(t, "GET", "/api/v1/repos/%s/repo16/commits?token="+token+"&page=2", user.Name)
@@ -121,7 +122,7 @@ func TestAPIReposGitCommitListDifferentBranch(t *testing.T) {
 	user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
 	// Login as User2.
 	session := loginUser(t, user.Name)
-	token := getTokenForLoggedInUser(t, session)
+	token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeReadRepository)
 
 	// Test getting commits (Page=1, Branch=good-sign)
 	req := NewRequestf(t, "GET", "/api/v1/repos/%s/repo16/commits?token="+token+"&sha=good-sign", user.Name)
@@ -140,7 +141,7 @@ func TestAPIReposGitCommitListWithoutSelectFields(t *testing.T) {
 	user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
 	// Login as User2.
 	session := loginUser(t, user.Name)
-	token := getTokenForLoggedInUser(t, session)
+	token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeReadRepository)
 
 	// Test getting commits without files, verification, and stats
 	req := NewRequestf(t, "GET", "/api/v1/repos/%s/repo16/commits?token="+token+"&sha=good-sign&stat=false&files=false&verification=false", user.Name)
@@ -161,7 +162,7 @@ func TestDownloadCommitDiffOrPatch(t *testing.T) {
 	user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
 	// Login as User2.
 	session := loginUser(t, user.Name)
-	token := getTokenForLoggedInUser(t, session)
+	token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeReadRepository)
 
 	// Test getting diff
 	reqDiff := NewRequestf(t, "GET", "/api/v1/repos/%s/repo16/git/commits/f27c2b2b03dcab38beaf89b0ab4ff61f6de63441.diff?token="+token, user.Name)
@@ -183,7 +184,7 @@ func TestGetFileHistory(t *testing.T) {
 	user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
 	// Login as User2.
 	session := loginUser(t, user.Name)
-	token := getTokenForLoggedInUser(t, session)
+	token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeReadRepository)
 
 	req := NewRequestf(t, "GET", "/api/v1/repos/%s/repo16/commits?path=readme.md&token="+token+"&sha=good-sign", user.Name)
 	resp := MakeRequest(t, req, http.StatusOK)
@@ -203,7 +204,7 @@ func TestGetFileHistoryNotOnMaster(t *testing.T) {
 	user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
 	// Login as User2.
 	session := loginUser(t, user.Name)
-	token := getTokenForLoggedInUser(t, session)
+	token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeReadRepository)
 
 	req := NewRequestf(t, "GET", "/api/v1/repos/%s/repo20/commits?path=test.csv&token="+token+"&sha=add-csv&not=master", user.Name)
 	resp := MakeRequest(t, req, http.StatusOK)
diff --git a/tests/integration/api_repo_git_hook_test.go b/tests/integration/api_repo_git_hook_test.go
index e1c4682e6d..9f3205ce60 100644
--- a/tests/integration/api_repo_git_hook_test.go
+++ b/tests/integration/api_repo_git_hook_test.go
@@ -31,7 +31,7 @@ func TestAPIListGitHooks(t *testing.T) {
 
 	// user1 is an admin user
 	session := loginUser(t, "user1")
-	token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeReadRepoHook)
+	token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeReadRepository)
 	req := NewRequestf(t, "GET", "/api/v1/repos/%s/%s/hooks/git?token=%s",
 		owner.Name, repo.Name, token)
 	resp := MakeRequest(t, req, http.StatusOK)
@@ -57,7 +57,7 @@ func TestAPIListGitHooksNoHooks(t *testing.T) {
 
 	// user1 is an admin user
 	session := loginUser(t, "user1")
-	token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeReadRepoHook)
+	token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeReadRepository)
 	req := NewRequestf(t, "GET", "/api/v1/repos/%s/%s/hooks/git?token=%s",
 		owner.Name, repo.Name, token)
 	resp := MakeRequest(t, req, http.StatusOK)
@@ -77,7 +77,7 @@ func TestAPIListGitHooksNoAccess(t *testing.T) {
 	owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
 
 	session := loginUser(t, owner.Name)
-	token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeReadRepoHook)
+	token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeReadRepository)
 	req := NewRequestf(t, "GET", "/api/v1/repos/%s/%s/hooks/git?token=%s",
 		owner.Name, repo.Name, token)
 	MakeRequest(t, req, http.StatusForbidden)
@@ -91,7 +91,7 @@ func TestAPIGetGitHook(t *testing.T) {
 
 	// user1 is an admin user
 	session := loginUser(t, "user1")
-	token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeReadRepoHook)
+	token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeReadRepository)
 	req := NewRequestf(t, "GET", "/api/v1/repos/%s/%s/hooks/git/pre-receive?token=%s",
 		owner.Name, repo.Name, token)
 	resp := MakeRequest(t, req, http.StatusOK)
@@ -108,7 +108,7 @@ func TestAPIGetGitHookNoAccess(t *testing.T) {
 	owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
 
 	session := loginUser(t, owner.Name)
-	token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeReadRepoHook)
+	token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeReadRepository)
 	req := NewRequestf(t, "GET", "/api/v1/repos/%s/%s/hooks/git/pre-receive?token=%s",
 		owner.Name, repo.Name, token)
 	MakeRequest(t, req, http.StatusForbidden)
@@ -122,7 +122,7 @@ func TestAPIEditGitHook(t *testing.T) {
 
 	// user1 is an admin user
 	session := loginUser(t, "user1")
-	token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeAdminRepoHook)
+	token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository)
 
 	urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/hooks/git/pre-receive?token=%s",
 		owner.Name, repo.Name, token)
@@ -151,7 +151,7 @@ func TestAPIEditGitHookNoAccess(t *testing.T) {
 	owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
 
 	session := loginUser(t, owner.Name)
-	token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepoHook)
+	token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository)
 	urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/hooks/git/pre-receive?token=%s",
 		owner.Name, repo.Name, token)
 	req := NewRequestWithJSON(t, "PATCH", urlStr, &api.EditGitHookOption{
@@ -168,7 +168,7 @@ func TestAPIDeleteGitHook(t *testing.T) {
 
 	// user1 is an admin user
 	session := loginUser(t, "user1")
-	token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeAdminRepoHook)
+	token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository)
 
 	req := NewRequestf(t, "DELETE", "/api/v1/repos/%s/%s/hooks/git/pre-receive?token=%s",
 		owner.Name, repo.Name, token)
@@ -190,7 +190,7 @@ func TestAPIDeleteGitHookNoAccess(t *testing.T) {
 	owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
 
 	session := loginUser(t, owner.Name)
-	token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepoHook)
+	token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository)
 	req := NewRequestf(t, "DELETE", "/api/v1/repos/%s/%s/hooks/git/pre-receive?token=%s",
 		owner.Name, repo.Name, token)
 	MakeRequest(t, req, http.StatusForbidden)
diff --git a/tests/integration/api_repo_git_notes_test.go b/tests/integration/api_repo_git_notes_test.go
index 1448f40572..30846f235f 100644
--- a/tests/integration/api_repo_git_notes_test.go
+++ b/tests/integration/api_repo_git_notes_test.go
@@ -8,6 +8,7 @@ import (
 	"net/url"
 	"testing"
 
+	auth_model "code.gitea.io/gitea/models/auth"
 	"code.gitea.io/gitea/models/unittest"
 	user_model "code.gitea.io/gitea/models/user"
 	api "code.gitea.io/gitea/modules/structs"
@@ -20,7 +21,7 @@ func TestAPIReposGitNotes(t *testing.T) {
 		user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
 		// Login as User2.
 		session := loginUser(t, user.Name)
-		token := getTokenForLoggedInUser(t, session)
+		token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeReadRepository)
 
 		// check invalid requests
 		req := NewRequestf(t, "GET", "/api/v1/repos/%s/repo1/git/notes/12345?token=%s", user.Name, token)
diff --git a/tests/integration/api_repo_git_ref_test.go b/tests/integration/api_repo_git_ref_test.go
index aac01ca9ad..20900b3241 100644
--- a/tests/integration/api_repo_git_ref_test.go
+++ b/tests/integration/api_repo_git_ref_test.go
@@ -7,6 +7,7 @@ import (
 	"net/http"
 	"testing"
 
+	auth_model "code.gitea.io/gitea/models/auth"
 	"code.gitea.io/gitea/models/unittest"
 	user_model "code.gitea.io/gitea/models/user"
 	"code.gitea.io/gitea/tests"
@@ -17,7 +18,7 @@ func TestAPIReposGitRefs(t *testing.T) {
 	user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
 	// Login as User2.
 	session := loginUser(t, user.Name)
-	token := getTokenForLoggedInUser(t, session)
+	token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeReadRepository)
 
 	for _, ref := range [...]string{
 		"refs/heads/master", // Branch
diff --git a/tests/integration/api_repo_git_tags_test.go b/tests/integration/api_repo_git_tags_test.go
index b29fc45cf5..a9d47abf93 100644
--- a/tests/integration/api_repo_git_tags_test.go
+++ b/tests/integration/api_repo_git_tags_test.go
@@ -26,7 +26,7 @@ func TestAPIGitTags(t *testing.T) {
 	repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
 	// Login as User2.
 	session := loginUser(t, user.Name)
-	token := getTokenForLoggedInUser(t, session)
+	token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeReadRepository)
 
 	// Set up git config for the tagger
 	_ = git.NewCommand(git.DefaultContext, "config", "user.name").AddDynamicArguments(user.Name).Run(&git.RunOpts{Dir: repo.RepoPath()})
@@ -70,7 +70,7 @@ func TestAPIDeleteTagByName(t *testing.T) {
 	repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
 	owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
 	session := loginUser(t, owner.LowerName)
-	token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeRepo)
+	token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository)
 
 	urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/tags/delete-tag?token=%s",
 		owner.Name, repo.Name, token)
diff --git a/tests/integration/api_repo_git_trees_test.go b/tests/integration/api_repo_git_trees_test.go
index d1d49e4627..7a7ece120c 100644
--- a/tests/integration/api_repo_git_trees_test.go
+++ b/tests/integration/api_repo_git_trees_test.go
@@ -7,6 +7,7 @@ import (
 	"net/http"
 	"testing"
 
+	auth_model "code.gitea.io/gitea/models/auth"
 	repo_model "code.gitea.io/gitea/models/repo"
 	"code.gitea.io/gitea/models/unittest"
 	user_model "code.gitea.io/gitea/models/user"
@@ -28,7 +29,7 @@ func TestAPIReposGitTrees(t *testing.T) {
 
 	// Login as User2.
 	session := loginUser(t, user2.Name)
-	token := getTokenForLoggedInUser(t, session)
+	token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeReadRepository)
 
 	// Test a public repo that anyone can GET the tree of
 	for _, ref := range [...]string{
diff --git a/tests/integration/api_repo_hook_test.go b/tests/integration/api_repo_hook_test.go
index 0fa2402992..18b12597db 100644
--- a/tests/integration/api_repo_hook_test.go
+++ b/tests/integration/api_repo_hook_test.go
@@ -26,7 +26,7 @@ func TestAPICreateHook(t *testing.T) {
 
 	// user1 is an admin user
 	session := loginUser(t, "user1")
-	token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepoHook)
+	token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository)
 	completeURL := func(lastSegment string) string {
 		return fmt.Sprintf("/api/v1/repos/%s/%s/%s?token=%s", owner.Name, repo.Name, lastSegment, token)
 	}
diff --git a/tests/integration/api_repo_lfs_migrate_test.go b/tests/integration/api_repo_lfs_migrate_test.go
index e66ca6b147..2cd7132b76 100644
--- a/tests/integration/api_repo_lfs_migrate_test.go
+++ b/tests/integration/api_repo_lfs_migrate_test.go
@@ -31,7 +31,7 @@ func TestAPIRepoLFSMigrateLocal(t *testing.T) {
 
 	user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
 	session := loginUser(t, user.Name)
-	token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeRepo)
+	token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository)
 
 	req := NewRequestWithJSON(t, "POST", "/api/v1/repos/migrate?token="+token, &api.MigrateRepoOptions{
 		CloneAddr:   path.Join(setting.RepoRootPath, "migration/lfs-test.git"),
diff --git a/tests/integration/api_repo_lfs_test.go b/tests/integration/api_repo_lfs_test.go
index a7a70baeef..b0e9269bb8 100644
--- a/tests/integration/api_repo_lfs_test.go
+++ b/tests/integration/api_repo_lfs_test.go
@@ -60,7 +60,7 @@ func TestAPILFSMediaType(t *testing.T) {
 }
 
 func createLFSTestRepository(t *testing.T, name string) *repo_model.Repository {
-	ctx := NewAPITestContext(t, "user2", "lfs-"+name+"-repo", auth_model.AccessTokenScopeRepo)
+	ctx := NewAPITestContext(t, "user2", "lfs-"+name+"-repo", auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser)
 	t.Run("CreateRepo", doAPICreateRepository(ctx, false))
 
 	repo, err := repo_model.GetRepositoryByOwnerAndName(db.DefaultContext, "user2", "lfs-"+name+"-repo")
diff --git a/tests/integration/api_repo_raw_test.go b/tests/integration/api_repo_raw_test.go
index 60e9eeed6b..ccb20a939e 100644
--- a/tests/integration/api_repo_raw_test.go
+++ b/tests/integration/api_repo_raw_test.go
@@ -20,7 +20,7 @@ func TestAPIReposRaw(t *testing.T) {
 	user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
 	// Login as User2.
 	session := loginUser(t, user.Name)
-	token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeRepo)
+	token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeReadRepository)
 
 	for _, ref := range [...]string{
 		"master", // Branch
diff --git a/tests/integration/api_repo_tags_test.go b/tests/integration/api_repo_tags_test.go
index d4fd9097dd..c4282f9928 100644
--- a/tests/integration/api_repo_tags_test.go
+++ b/tests/integration/api_repo_tags_test.go
@@ -23,7 +23,7 @@ func TestAPIRepoTags(t *testing.T) {
 	user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
 	// Login as User2.
 	session := loginUser(t, user.Name)
-	token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeRepo)
+	token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository)
 
 	repoName := "repo1"
 
diff --git a/tests/integration/api_repo_teams_test.go b/tests/integration/api_repo_teams_test.go
index 1f444e3141..3d3200b291 100644
--- a/tests/integration/api_repo_teams_test.go
+++ b/tests/integration/api_repo_teams_test.go
@@ -28,7 +28,7 @@ func TestAPIRepoTeams(t *testing.T) {
 	// user4
 	user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4})
 	session := loginUser(t, user.Name)
-	token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeRepo)
+	token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository)
 
 	// ListTeams
 	url := fmt.Sprintf("/api/v1/repos/%s/teams?token=%s", publicOrgRepo.FullName(), token)
@@ -68,7 +68,7 @@ func TestAPIRepoTeams(t *testing.T) {
 	// AddTeam with user2
 	user = unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
 	session = loginUser(t, user.Name)
-	token = getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeRepo)
+	token = getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository)
 	url = fmt.Sprintf("/api/v1/repos/%s/teams/%s?token=%s", publicOrgRepo.FullName(), "team1", token)
 	req = NewRequest(t, "PUT", url)
 	MakeRequest(t, req, http.StatusNoContent)
diff --git a/tests/integration/api_repo_test.go b/tests/integration/api_repo_test.go
index aa4e674206..0f387192eb 100644
--- a/tests/integration/api_repo_test.go
+++ b/tests/integration/api_repo_test.go
@@ -189,7 +189,7 @@ func TestAPISearchRepo(t *testing.T) {
 				if userToLogin != nil && userToLogin.ID > 0 {
 					testName = fmt.Sprintf("LoggedUser%d", userToLogin.ID)
 					session := loginUser(t, userToLogin.Name)
-					token = getTokenForLoggedInUser(t, session)
+					token = getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeReadRepository)
 					userID = userToLogin.ID
 				} else {
 					testName = "AnonymousUser"
@@ -295,7 +295,7 @@ func TestAPIOrgRepos(t *testing.T) {
 	for userToLogin, expected := range expectedResults {
 		testName := fmt.Sprintf("LoggedUser%d", userToLogin.ID)
 		session := loginUser(t, userToLogin.Name)
-		token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeReadOrg)
+		token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeReadOrganization)
 
 		t.Run(testName, func(t *testing.T) {
 			req := NewRequestf(t, "GET", "/api/v1/orgs/%s/repos?token="+token, sourceOrg.Name)
@@ -317,7 +317,7 @@ func TestAPIGetRepoByIDUnauthorized(t *testing.T) {
 	defer tests.PrepareTestEnv(t)()
 	user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4})
 	session := loginUser(t, user.Name)
-	token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeRepo)
+	token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeReadRepository)
 	req := NewRequestf(t, "GET", "/api/v1/repositories/2?token="+token)
 	MakeRequest(t, req, http.StatusNotFound)
 }
@@ -341,7 +341,7 @@ func TestAPIRepoMigrate(t *testing.T) {
 	for _, testCase := range testCases {
 		user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: testCase.ctxUserID})
 		session := loginUser(t, user.Name)
-		token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeRepo)
+		token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository)
 		req := NewRequestWithJSON(t, "POST", "/api/v1/repos/migrate?token="+token, &api.MigrateRepoOptions{
 			CloneAddr:   testCase.cloneURL,
 			RepoOwnerID: testCase.userID,
@@ -371,7 +371,7 @@ func TestAPIRepoMigrateConflict(t *testing.T) {
 
 func testAPIRepoMigrateConflict(t *testing.T, u *url.URL) {
 	username := "user2"
-	baseAPITestContext := NewAPITestContext(t, username, "repo1", auth_model.AccessTokenScopeRepo)
+	baseAPITestContext := NewAPITestContext(t, username, "repo1", auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser)
 
 	u.Path = baseAPITestContext.GitPath()
 
@@ -406,7 +406,7 @@ func TestAPIMirrorSyncNonMirrorRepo(t *testing.T) {
 	defer tests.PrepareTestEnv(t)()
 
 	session := loginUser(t, "user2")
-	token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeRepo)
+	token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository)
 
 	var repo api.Repository
 	req := NewRequest(t, "GET", "/api/v1/repos/user2/repo1")
@@ -438,7 +438,7 @@ func TestAPIOrgRepoCreate(t *testing.T) {
 	for _, testCase := range testCases {
 		user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: testCase.ctxUserID})
 		session := loginUser(t, user.Name)
-		token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeAdminOrg)
+		token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteOrganization, auth_model.AccessTokenScopeWriteRepository)
 		req := NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/org/%s/repos?token="+token, testCase.orgName), &api.CreateRepoOption{
 			Name: testCase.repoName,
 		})
@@ -452,7 +452,7 @@ func TestAPIRepoCreateConflict(t *testing.T) {
 
 func testAPIRepoCreateConflict(t *testing.T, u *url.URL) {
 	username := "user2"
-	baseAPITestContext := NewAPITestContext(t, username, "repo1", auth_model.AccessTokenScopeRepo)
+	baseAPITestContext := NewAPITestContext(t, username, "repo1", auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser)
 
 	u.Path = baseAPITestContext.GitPath()
 
@@ -502,7 +502,7 @@ func TestAPIRepoTransfer(t *testing.T) {
 	// create repo to move
 	user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
 	session := loginUser(t, user.Name)
-	token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeRepo)
+	token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser)
 	repoName := "moveME"
 	apiRepo := new(api.Repository)
 	req := NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/user/repos?token=%s", token), &api.CreateRepoOption{
@@ -520,7 +520,7 @@ func TestAPIRepoTransfer(t *testing.T) {
 		user = unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: testCase.ctxUserID})
 		repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: apiRepo.ID})
 		session = loginUser(t, user.Name)
-		token = getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeRepo)
+		token = getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository)
 		req = NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/repos/%s/%s/transfer?token=%s", repo.OwnerName, repo.Name, token), &api.TransferRepoOption{
 			NewOwner: testCase.newOwner,
 			TeamIDs:  testCase.teams,
@@ -537,7 +537,7 @@ func transfer(t *testing.T) *repo_model.Repository {
 	// create repo to move
 	user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
 	session := loginUser(t, user.Name)
-	token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeRepo)
+	token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser)
 	repoName := "moveME"
 	apiRepo := new(api.Repository)
 	req := NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/user/repos?token=%s", token), &api.CreateRepoOption{
@@ -567,7 +567,7 @@ func TestAPIAcceptTransfer(t *testing.T) {
 
 	// try to accept with not authorized user
 	session := loginUser(t, "user2")
-	token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeRepo)
+	token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser)
 	req := NewRequest(t, "POST", fmt.Sprintf("/api/v1/repos/%s/%s/transfer/reject?token=%s", repo.OwnerName, repo.Name, token))
 	MakeRequest(t, req, http.StatusForbidden)
 
@@ -577,7 +577,7 @@ func TestAPIAcceptTransfer(t *testing.T) {
 
 	// accept transfer
 	session = loginUser(t, "user4")
-	token = getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeRepo)
+	token = getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser)
 
 	req = NewRequest(t, "POST", fmt.Sprintf("/api/v1/repos/%s/%s/transfer/accept?token=%s", repo.OwnerName, repo.Name, token))
 	resp := MakeRequest(t, req, http.StatusAccepted)
@@ -593,7 +593,7 @@ func TestAPIRejectTransfer(t *testing.T) {
 
 	// try to reject with not authorized user
 	session := loginUser(t, "user2")
-	token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeRepo)
+	token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository)
 	req := NewRequest(t, "POST", fmt.Sprintf("/api/v1/repos/%s/%s/transfer/reject?token=%s", repo.OwnerName, repo.Name, token))
 	MakeRequest(t, req, http.StatusForbidden)
 
@@ -603,7 +603,7 @@ func TestAPIRejectTransfer(t *testing.T) {
 
 	// reject transfer
 	session = loginUser(t, "user4")
-	token = getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeRepo)
+	token = getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository)
 
 	req = NewRequest(t, "POST", fmt.Sprintf("/api/v1/repos/%s/%s/transfer/reject?token=%s", repo.OwnerName, repo.Name, token))
 	resp := MakeRequest(t, req, http.StatusOK)
@@ -617,7 +617,7 @@ func TestAPIGenerateRepo(t *testing.T) {
 
 	user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
 	session := loginUser(t, user.Name)
-	token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeRepo)
+	token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository)
 
 	templateRepo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 44})
 
@@ -653,7 +653,7 @@ func TestAPIRepoGetReviewers(t *testing.T) {
 	defer tests.PrepareTestEnv(t)()
 	user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
 	session := loginUser(t, user.Name)
-	token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeRepo)
+	token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeReadRepository)
 	repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
 
 	req := NewRequestf(t, "GET", "/api/v1/repos/%s/%s/reviewers?token=%s", user.Name, repo.Name, token)
@@ -667,7 +667,7 @@ func TestAPIRepoGetAssignees(t *testing.T) {
 	defer tests.PrepareTestEnv(t)()
 	user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
 	session := loginUser(t, user.Name)
-	token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeRepo)
+	token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeReadRepository)
 	repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
 
 	req := NewRequestf(t, "GET", "/api/v1/repos/%s/%s/assignees?token=%s", user.Name, repo.Name, token)
diff --git a/tests/integration/api_repo_topic_test.go b/tests/integration/api_repo_topic_test.go
index ab9fd9bb96..251b9a3b65 100644
--- a/tests/integration/api_repo_topic_test.go
+++ b/tests/integration/api_repo_topic_test.go
@@ -60,7 +60,7 @@ func TestAPIRepoTopic(t *testing.T) {
 	repo3 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 3})
 
 	// Get user2's token
-	token2 := getUserToken(t, user2.Name, auth_model.AccessTokenScopeRepo)
+	token2 := getUserToken(t, user2.Name, auth_model.AccessTokenScopeWriteRepository)
 
 	// Test read topics using login
 	url := fmt.Sprintf("/api/v1/repos/%s/%s/topics", user2.Name, repo2.Name)
@@ -140,7 +140,7 @@ func TestAPIRepoTopic(t *testing.T) {
 	MakeRequest(t, req, http.StatusNotFound)
 
 	// Get user4's token
-	token4 := getUserToken(t, user4.Name, auth_model.AccessTokenScopeRepo)
+	token4 := getUserToken(t, user4.Name, auth_model.AccessTokenScopeWriteRepository)
 
 	// Test read topics with write access
 	url = fmt.Sprintf("/api/v1/repos/%s/%s/topics?token=%s", user3.Name, repo3.Name, token4)
diff --git a/tests/integration/api_team_test.go b/tests/integration/api_team_test.go
index 60c61394d4..a9ae890717 100644
--- a/tests/integration/api_team_test.go
+++ b/tests/integration/api_team_test.go
@@ -33,7 +33,7 @@ func TestAPITeam(t *testing.T) {
 	user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: teamUser.UID})
 
 	session := loginUser(t, user.Name)
-	token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeAdminOrg)
+	token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeReadOrganization)
 	req := NewRequestf(t, "GET", "/api/v1/teams/%d?token="+token, teamUser.TeamID)
 	resp := MakeRequest(t, req, http.StatusOK)
 
@@ -48,7 +48,7 @@ func TestAPITeam(t *testing.T) {
 	user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: teamUser2.UID})
 
 	session = loginUser(t, user2.Name)
-	token = getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeReadOrg)
+	token = getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeReadOrganization)
 	req = NewRequestf(t, "GET", "/api/v1/teams/%d?token="+token, teamUser.TeamID)
 	_ = MakeRequest(t, req, http.StatusForbidden)
 
@@ -58,7 +58,7 @@ func TestAPITeam(t *testing.T) {
 	// Get an admin user able to create, update and delete teams.
 	user = unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
 	session = loginUser(t, user.Name)
-	token = getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeAdminOrg)
+	token = getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteOrganization)
 
 	org = unittest.AssertExistsAndLoadBean(t, &organization.Organization{ID: 6})
 
@@ -262,7 +262,7 @@ func TestAPITeamSearch(t *testing.T) {
 
 	var results TeamSearchResults
 
-	token := getUserToken(t, user.Name, auth_model.AccessTokenScopeReadOrg)
+	token := getUserToken(t, user.Name, auth_model.AccessTokenScopeReadOrganization)
 	req := NewRequestf(t, "GET", "/api/v1/orgs/%s/teams/search?q=%s&token=%s", org.Name, "_team", token)
 	resp := MakeRequest(t, req, http.StatusOK)
 	DecodeJSON(t, resp, &results)
@@ -272,7 +272,7 @@ func TestAPITeamSearch(t *testing.T) {
 
 	// no access if not organization member
 	user5 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 5})
-	token5 := getUserToken(t, user5.Name, auth_model.AccessTokenScopeReadOrg)
+	token5 := getUserToken(t, user5.Name, auth_model.AccessTokenScopeReadOrganization)
 
 	req = NewRequestf(t, "GET", "/api/v1/orgs/%s/teams/search?q=%s&token=%s", org.Name, "team", token5)
 	MakeRequest(t, req, http.StatusForbidden)
@@ -287,7 +287,7 @@ func TestAPIGetTeamRepo(t *testing.T) {
 
 	var results api.Repository
 
-	token := getUserToken(t, user.Name, auth_model.AccessTokenScopeReadOrg)
+	token := getUserToken(t, user.Name, auth_model.AccessTokenScopeReadOrganization)
 	req := NewRequestf(t, "GET", "/api/v1/teams/%d/repos/%s/?token=%s", team.ID, teamRepo.FullName(), token)
 	resp := MakeRequest(t, req, http.StatusOK)
 	DecodeJSON(t, resp, &results)
@@ -295,7 +295,7 @@ func TestAPIGetTeamRepo(t *testing.T) {
 
 	// no access if not organization member
 	user5 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 5})
-	token5 := getUserToken(t, user5.Name, auth_model.AccessTokenScopeReadOrg)
+	token5 := getUserToken(t, user5.Name, auth_model.AccessTokenScopeReadOrganization)
 
 	req = NewRequestf(t, "GET", "/api/v1/teams/%d/repos/%s/?token=%s", team.ID, teamRepo.FullName(), token5)
 	MakeRequest(t, req, http.StatusNotFound)
diff --git a/tests/integration/api_team_user_test.go b/tests/integration/api_team_user_test.go
index 468697a393..aa33c69041 100644
--- a/tests/integration/api_team_user_test.go
+++ b/tests/integration/api_team_user_test.go
@@ -24,7 +24,7 @@ func TestAPITeamUser(t *testing.T) {
 
 	normalUsername := "user2"
 	session := loginUser(t, normalUsername)
-	token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeReadOrg)
+	token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeReadOrganization)
 	req := NewRequest(t, "GET", "/api/v1/teams/1/members/user1?token="+token)
 	MakeRequest(t, req, http.StatusNotFound)
 
diff --git a/tests/integration/api_token_test.go b/tests/integration/api_token_test.go
index 498bd41b0f..0eecccb653 100644
--- a/tests/integration/api_token_test.go
+++ b/tests/integration/api_token_test.go
@@ -4,14 +4,18 @@
 package integration
 
 import (
+	"fmt"
 	"net/http"
 	"testing"
 
 	auth_model "code.gitea.io/gitea/models/auth"
 	"code.gitea.io/gitea/models/unittest"
 	user_model "code.gitea.io/gitea/models/user"
+	"code.gitea.io/gitea/modules/log"
 	api "code.gitea.io/gitea/modules/structs"
 	"code.gitea.io/gitea/tests"
+
+	"github.com/stretchr/testify/assert"
 )
 
 // TestAPICreateAndDeleteToken tests that token that was just created can be deleted
@@ -19,9 +23,518 @@ func TestAPICreateAndDeleteToken(t *testing.T) {
 	defer tests.PrepareTestEnv(t)()
 	user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
 
-	req := NewRequestWithJSON(t, "POST", "/api/v1/users/user1/tokens", map[string]string{
-		"name": "test-key-1",
+	newAccessToken := createAPIAccessTokenWithoutCleanUp(t, "test-key-1", user, nil)
+	deleteAPIAccessToken(t, newAccessToken, user)
+
+	newAccessToken = createAPIAccessTokenWithoutCleanUp(t, "test-key-2", user, nil)
+	deleteAPIAccessToken(t, newAccessToken, user)
+}
+
+// TestAPIDeleteMissingToken ensures that error is thrown when token not found
+func TestAPIDeleteMissingToken(t *testing.T) {
+	defer tests.PrepareTestEnv(t)()
+	user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
+
+	req := NewRequestf(t, "DELETE", "/api/v1/users/user1/tokens/%d", unittest.NonexistentID)
+	req = AddBasicAuthHeader(req, user.Name)
+	MakeRequest(t, req, http.StatusNotFound)
+}
+
+type permission struct {
+	category auth_model.AccessTokenScopeCategory
+	level    auth_model.AccessTokenScopeLevel
+}
+
+type requiredScopeTestCase struct {
+	url                 string
+	method              string
+	requiredPermissions []permission
+}
+
+func (c *requiredScopeTestCase) Name() string {
+	return fmt.Sprintf("%v %v", c.method, c.url)
+}
+
+// TestAPIDeniesPermissionBasedOnTokenScope tests that API routes forbid access
+// when the correct token scope is not included.
+func TestAPIDeniesPermissionBasedOnTokenScope(t *testing.T) {
+	defer tests.PrepareTestEnv(t)()
+
+	// We'll assert that each endpoint, when fetched with a token with all
+	// scopes *except* the ones specified, a forbidden status code is returned.
+	//
+	// This is to protect against endpoints having their access check copied
+	// from other endpoints and not updated.
+	//
+	// Test cases are in alphabetical order by URL.
+	//
+	// Note: query parameters are not currently supported since the token is
+	// appended with `?=token=<token>`.
+	testCases := []requiredScopeTestCase{
+		{
+			"/api/v1/admin/emails",
+			"GET",
+			[]permission{
+				{
+					auth_model.AccessTokenScopeCategoryAdmin,
+					auth_model.Read,
+				},
+			},
+		},
+		{
+			"/api/v1/admin/users",
+			"GET",
+			[]permission{
+				{
+					auth_model.AccessTokenScopeCategoryAdmin,
+					auth_model.Read,
+				},
+			},
+		},
+		{
+			"/api/v1/admin/users",
+			"POST",
+			[]permission{
+				{
+					auth_model.AccessTokenScopeCategoryAdmin,
+					auth_model.Write,
+				},
+			},
+		},
+		{
+			"/api/v1/admin/users/user2",
+			"PATCH",
+			[]permission{
+				{
+					auth_model.AccessTokenScopeCategoryAdmin,
+					auth_model.Write,
+				},
+			},
+		},
+		{
+			"/api/v1/admin/users/user2/orgs",
+			"GET",
+			[]permission{
+				{
+					auth_model.AccessTokenScopeCategoryAdmin,
+					auth_model.Read,
+				},
+			},
+		},
+		{
+			"/api/v1/admin/users/user2/orgs",
+			"POST",
+			[]permission{
+				{
+					auth_model.AccessTokenScopeCategoryAdmin,
+					auth_model.Write,
+				},
+			},
+		},
+		{
+			"/api/v1/admin/orgs",
+			"GET",
+			[]permission{
+				{
+					auth_model.AccessTokenScopeCategoryAdmin,
+					auth_model.Read,
+				},
+			},
+		},
+		{
+			"/api/v1/markdown",
+			"POST",
+			[]permission{
+				{
+					auth_model.AccessTokenScopeCategoryMisc,
+					auth_model.Write,
+				},
+			},
+		},
+		{
+			"/api/v1/markdown/raw",
+			"POST",
+			[]permission{
+				{
+					auth_model.AccessTokenScopeCategoryMisc,
+					auth_model.Write,
+				},
+			},
+		},
+		{
+			"/api/v1/notifications",
+			"GET",
+			[]permission{
+				{
+					auth_model.AccessTokenScopeCategoryNotification,
+					auth_model.Read,
+				},
+			},
+		},
+		{
+			"/api/v1/notifications",
+			"PUT",
+			[]permission{
+				{
+					auth_model.AccessTokenScopeCategoryNotification,
+					auth_model.Write,
+				},
+			},
+		},
+		{
+			"/api/v1/org/org1/repos",
+			"POST",
+			[]permission{
+				{
+					auth_model.AccessTokenScopeCategoryOrganization,
+					auth_model.Write,
+				},
+				{
+					auth_model.AccessTokenScopeCategoryRepository,
+					auth_model.Write,
+				},
+			},
+		},
+		{
+			"/api/v1/packages/user1/type/name/1",
+			"GET",
+			[]permission{
+				{
+					auth_model.AccessTokenScopeCategoryPackage,
+					auth_model.Read,
+				},
+			},
+		},
+		{
+			"/api/v1/packages/user1/type/name/1",
+			"DELETE",
+			[]permission{
+				{
+					auth_model.AccessTokenScopeCategoryPackage,
+					auth_model.Write,
+				},
+			},
+		},
+		{
+			"/api/v1/repos/user1/repo1",
+			"GET",
+			[]permission{
+				{
+					auth_model.AccessTokenScopeCategoryRepository,
+					auth_model.Read,
+				},
+			},
+		},
+		{
+			"/api/v1/repos/user1/repo1",
+			"PATCH",
+			[]permission{
+				{
+					auth_model.AccessTokenScopeCategoryRepository,
+					auth_model.Write,
+				},
+			},
+		},
+		{
+			"/api/v1/repos/user1/repo1",
+			"DELETE",
+			[]permission{
+				{
+					auth_model.AccessTokenScopeCategoryRepository,
+					auth_model.Write,
+				},
+			},
+		},
+		{
+			"/api/v1/repos/user1/repo1/branches",
+			"GET",
+			[]permission{
+				{
+					auth_model.AccessTokenScopeCategoryRepository,
+					auth_model.Read,
+				},
+			},
+		},
+		{
+			"/api/v1/repos/user1/repo1/archive/foo",
+			"GET",
+			[]permission{
+				{
+					auth_model.AccessTokenScopeCategoryRepository,
+					auth_model.Read,
+				},
+			},
+		},
+		{
+			"/api/v1/repos/user1/repo1/issues",
+			"GET",
+			[]permission{
+				{
+					auth_model.AccessTokenScopeCategoryIssue,
+					auth_model.Read,
+				},
+			},
+		},
+		{
+			"/api/v1/repos/user1/repo1/media/foo",
+			"GET",
+			[]permission{
+				{
+					auth_model.AccessTokenScopeCategoryRepository,
+					auth_model.Read,
+				},
+			},
+		},
+		{
+			"/api/v1/repos/user1/repo1/raw/foo",
+			"GET",
+			[]permission{
+				{
+					auth_model.AccessTokenScopeCategoryRepository,
+					auth_model.Read,
+				},
+			},
+		},
+		{
+			"/api/v1/repos/user1/repo1/teams",
+			"GET",
+			[]permission{
+				{
+					auth_model.AccessTokenScopeCategoryRepository,
+					auth_model.Read,
+				},
+			},
+		},
+		{
+			"/api/v1/repos/user1/repo1/teams/team1",
+			"PUT",
+			[]permission{
+				{
+					auth_model.AccessTokenScopeCategoryRepository,
+					auth_model.Write,
+				},
+			},
+		},
+		{
+			"/api/v1/repos/user1/repo1/transfer",
+			"POST",
+			[]permission{
+				{
+					auth_model.AccessTokenScopeCategoryRepository,
+					auth_model.Write,
+				},
+			},
+		},
+		// Private repo
+		{
+			"/api/v1/repos/user2/repo2",
+			"GET",
+			[]permission{
+				{
+					auth_model.AccessTokenScopeCategoryRepository,
+					auth_model.Read,
+				},
+			},
+		},
+		// Private repo
+		{
+			"/api/v1/repos/user2/repo2",
+			"GET",
+			[]permission{
+				{
+					auth_model.AccessTokenScopeCategoryRepository,
+					auth_model.Read,
+				},
+			},
+		},
+		{
+			"/api/v1/settings/api",
+			"GET",
+			[]permission{
+				{
+					auth_model.AccessTokenScopeCategoryMisc,
+					auth_model.Read,
+				},
+			},
+		},
+		{
+			"/api/v1/user",
+			"GET",
+			[]permission{
+				{
+					auth_model.AccessTokenScopeCategoryUser,
+					auth_model.Read,
+				},
+			},
+		},
+		{
+			"/api/v1/user/emails",
+			"GET",
+			[]permission{
+				{
+					auth_model.AccessTokenScopeCategoryUser,
+					auth_model.Read,
+				},
+			},
+		},
+		{
+			"/api/v1/user/emails",
+			"POST",
+			[]permission{
+				{
+					auth_model.AccessTokenScopeCategoryUser,
+					auth_model.Write,
+				},
+			},
+		},
+		{
+			"/api/v1/user/emails",
+			"DELETE",
+			[]permission{
+				{
+					auth_model.AccessTokenScopeCategoryUser,
+					auth_model.Write,
+				},
+			},
+		},
+		{
+			"/api/v1/user/applications/oauth2",
+			"GET",
+			[]permission{
+				{
+					auth_model.AccessTokenScopeCategoryUser,
+					auth_model.Read,
+				},
+			},
+		},
+		{
+			"/api/v1/user/applications/oauth2",
+			"POST",
+			[]permission{
+				{
+					auth_model.AccessTokenScopeCategoryUser,
+					auth_model.Write,
+				},
+			},
+		},
+		{
+			"/api/v1/users/search",
+			"GET",
+			[]permission{
+				{
+					auth_model.AccessTokenScopeCategoryUser,
+					auth_model.Read,
+				},
+			},
+		},
+		// Private user
+		{
+			"/api/v1/users/user31",
+			"GET",
+			[]permission{
+				{
+					auth_model.AccessTokenScopeCategoryUser,
+					auth_model.Read,
+				},
+			},
+		},
+		// Private user
+		{
+			"/api/v1/users/user31/gpg_keys",
+			"GET",
+			[]permission{
+				{
+					auth_model.AccessTokenScopeCategoryUser,
+					auth_model.Read,
+				},
+			},
+		},
+	}
+
+	// User needs to be admin so that we can verify that tokens without admin
+	// scopes correctly deny access.
+	user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
+	assert.True(t, user.IsAdmin, "User needs to be admin")
+
+	for _, testCase := range testCases {
+		runTestCase(t, &testCase, user)
+	}
+}
+
+// runTestCase Helper function to run a single test case.
+func runTestCase(t *testing.T, testCase *requiredScopeTestCase, user *user_model.User) {
+	t.Run(testCase.Name(), func(t *testing.T) {
+		defer tests.PrintCurrentTest(t)()
+
+		// Create a token with all scopes NOT required by the endpoint.
+		var unauthorizedScopes []auth_model.AccessTokenScope
+		for _, category := range auth_model.AllAccessTokenScopeCategories {
+			// For permissions, Write > Read > NoAccess.  So we need to
+			// find the minimum required, and only grant permission up to but
+			// not including the minimum required.
+			minRequiredLevel := auth_model.Write
+			categoryIsRequired := false
+			for _, requiredPermission := range testCase.requiredPermissions {
+				if requiredPermission.category != category {
+					continue
+				}
+				categoryIsRequired = true
+				if requiredPermission.level < minRequiredLevel {
+					minRequiredLevel = requiredPermission.level
+				}
+			}
+			unauthorizedLevel := auth_model.Write
+			if categoryIsRequired {
+				if minRequiredLevel == auth_model.Read {
+					unauthorizedLevel = auth_model.NoAccess
+				} else if minRequiredLevel == auth_model.Write {
+					unauthorizedLevel = auth_model.Read
+				} else {
+					assert.Failf(t, "Invalid test case", "Unknown access token scope level: %v", minRequiredLevel)
+					return
+				}
+			}
+
+			if unauthorizedLevel == auth_model.NoAccess {
+				continue
+			}
+			cateogoryUnauthorizedScopes := auth_model.GetRequiredScopes(
+				unauthorizedLevel,
+				category)
+			unauthorizedScopes = append(unauthorizedScopes, cateogoryUnauthorizedScopes...)
+		}
+
+		accessToken := createAPIAccessTokenWithoutCleanUp(t, "test-token", user, &unauthorizedScopes)
+		defer deleteAPIAccessToken(t, accessToken, user)
+
+		// Add API access token to the URL.
+		url := fmt.Sprintf("%s?token=%s", testCase.url, accessToken.Token)
+
+		// Request the endpoint.  Verify that permission is denied.
+		req := NewRequestf(t, testCase.method, url)
+		MakeRequest(t, req, http.StatusForbidden)
 	})
+}
+
+// createAPIAccessTokenWithoutCleanUp Create an API access token and assert that
+// creation succeeded.  The caller is responsible for deleting the token.
+func createAPIAccessTokenWithoutCleanUp(t *testing.T, tokenName string, user *user_model.User, scopes *[]auth_model.AccessTokenScope) api.AccessToken {
+	payload := map[string]interface{}{
+		"name": tokenName,
+	}
+	if scopes != nil {
+		for _, scope := range *scopes {
+			scopes, scopesExists := payload["scopes"].([]string)
+			if !scopesExists {
+				scopes = make([]string, 0)
+			}
+			scopes = append(scopes, string(scope))
+			payload["scopes"] = scopes
+		}
+	}
+	log.Debug("Requesting creation of token with scopes: %v", scopes)
+	req := NewRequestWithJSON(t, "POST", "/api/v1/users/user1/tokens", payload)
+
 	req = AddBasicAuthHeader(req, user.Name)
 	resp := MakeRequest(t, req, http.StatusCreated)
 
@@ -34,32 +547,15 @@ func TestAPICreateAndDeleteToken(t *testing.T) {
 		UID:   user.ID,
 	})
 
-	req = NewRequestf(t, "DELETE", "/api/v1/users/user1/tokens/%d", newAccessToken.ID)
+	return newAccessToken
+}
+
+// createAPIAccessTokenWithoutCleanUp Delete an API access token and assert that
+// deletion succeeded.
+func deleteAPIAccessToken(t *testing.T, accessToken api.AccessToken, user *user_model.User) {
+	req := NewRequestf(t, "DELETE", "/api/v1/users/user1/tokens/%d", accessToken.ID)
 	req = AddBasicAuthHeader(req, user.Name)
 	MakeRequest(t, req, http.StatusNoContent)
 
-	unittest.AssertNotExistsBean(t, &auth_model.AccessToken{ID: newAccessToken.ID})
-
-	req = NewRequestWithJSON(t, "POST", "/api/v1/users/user1/tokens", map[string]string{
-		"name": "test-key-2",
-	})
-	req = AddBasicAuthHeader(req, user.Name)
-	resp = MakeRequest(t, req, http.StatusCreated)
-	DecodeJSON(t, resp, &newAccessToken)
-
-	req = NewRequestf(t, "DELETE", "/api/v1/users/user1/tokens/%s", newAccessToken.Name)
-	req = AddBasicAuthHeader(req, user.Name)
-	MakeRequest(t, req, http.StatusNoContent)
-
-	unittest.AssertNotExistsBean(t, &auth_model.AccessToken{ID: newAccessToken.ID})
-}
-
-// TestAPIDeleteMissingToken ensures that error is thrown when token not found
-func TestAPIDeleteMissingToken(t *testing.T) {
-	defer tests.PrepareTestEnv(t)()
-	user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
-
-	req := NewRequestf(t, "DELETE", "/api/v1/users/user1/tokens/%d", unittest.NonexistentID)
-	req = AddBasicAuthHeader(req, user.Name)
-	MakeRequest(t, req, http.StatusNotFound)
+	unittest.AssertNotExistsBean(t, &auth_model.AccessToken{ID: accessToken.ID})
 }
diff --git a/tests/integration/api_user_email_test.go b/tests/integration/api_user_email_test.go
index 09083d9ce8..9fff42af42 100644
--- a/tests/integration/api_user_email_test.go
+++ b/tests/integration/api_user_email_test.go
@@ -46,7 +46,7 @@ func TestAPIAddEmail(t *testing.T) {
 
 	normalUsername := "user2"
 	session := loginUser(t, normalUsername)
-	token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeUser)
+	token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteUser)
 
 	opts := api.CreateEmailOption{
 		Emails: []string{"user101@example.com"},
@@ -83,7 +83,7 @@ func TestAPIDeleteEmail(t *testing.T) {
 
 	normalUsername := "user2"
 	session := loginUser(t, normalUsername)
-	token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeUser)
+	token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteUser)
 
 	opts := api.DeleteEmailOption{
 		Emails: []string{"user2-3@example.com"},
diff --git a/tests/integration/api_user_follow_test.go b/tests/integration/api_user_follow_test.go
index c7ad62e649..62717af90e 100644
--- a/tests/integration/api_user_follow_test.go
+++ b/tests/integration/api_user_follow_test.go
@@ -22,10 +22,10 @@ func TestAPIFollow(t *testing.T) {
 	user2 := "user1"
 
 	session1 := loginUser(t, user1)
-	token1 := getTokenForLoggedInUser(t, session1)
+	token1 := getTokenForLoggedInUser(t, session1, auth_model.AccessTokenScopeReadUser)
 
 	session2 := loginUser(t, user2)
-	token2 := getTokenForLoggedInUser(t, session2, auth_model.AccessTokenScopeUserFollow)
+	token2 := getTokenForLoggedInUser(t, session2, auth_model.AccessTokenScopeWriteUser)
 
 	t.Run("Follow", func(t *testing.T) {
 		defer tests.PrintCurrentTest(t)()
diff --git a/tests/integration/api_user_heatmap_test.go b/tests/integration/api_user_heatmap_test.go
index 432306914c..5a7e58a02d 100644
--- a/tests/integration/api_user_heatmap_test.go
+++ b/tests/integration/api_user_heatmap_test.go
@@ -10,6 +10,7 @@ import (
 	"time"
 
 	activities_model "code.gitea.io/gitea/models/activities"
+	auth_model "code.gitea.io/gitea/models/auth"
 	"code.gitea.io/gitea/modules/timeutil"
 	"code.gitea.io/gitea/tests"
 
@@ -20,7 +21,7 @@ func TestUserHeatmap(t *testing.T) {
 	defer tests.PrepareTestEnv(t)()
 	adminUsername := "user1"
 	normalUsername := "user2"
-	token := getUserToken(t, adminUsername)
+	token := getUserToken(t, adminUsername, auth_model.AccessTokenScopeReadUser)
 
 	fakeNow := time.Date(2011, 10, 20, 0, 0, 0, 0, time.Local)
 	timeutil.Set(fakeNow)
diff --git a/tests/integration/api_user_info_test.go b/tests/integration/api_user_info_test.go
index 65262792b7..82cd97e904 100644
--- a/tests/integration/api_user_info_test.go
+++ b/tests/integration/api_user_info_test.go
@@ -8,6 +8,7 @@ import (
 	"net/http"
 	"testing"
 
+	auth_model "code.gitea.io/gitea/models/auth"
 	api "code.gitea.io/gitea/modules/structs"
 	"code.gitea.io/gitea/tests"
 
@@ -21,7 +22,7 @@ func TestAPIUserInfo(t *testing.T) {
 	user2 := "user31"
 
 	session := loginUser(t, user)
-	token := getTokenForLoggedInUser(t, session)
+	token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeReadUser)
 
 	t.Run("GetInfo", func(t *testing.T) {
 		defer tests.PrintCurrentTest(t)()
diff --git a/tests/integration/api_user_org_perm_test.go b/tests/integration/api_user_org_perm_test.go
index ac575b1f01..40870f39ff 100644
--- a/tests/integration/api_user_org_perm_test.go
+++ b/tests/integration/api_user_org_perm_test.go
@@ -33,7 +33,7 @@ func sampleTest(t *testing.T, auoptc apiUserOrgPermTestCase) {
 	defer tests.PrepareTestEnv(t)()
 
 	session := loginUser(t, auoptc.LoginUser)
-	token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeReadOrg)
+	token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeReadOrganization, auth_model.AccessTokenScopeReadUser)
 
 	req := NewRequest(t, "GET", fmt.Sprintf("/api/v1/users/%s/orgs/%s/permissions?token=%s", auoptc.User, auoptc.Organization, token))
 	resp := MakeRequest(t, req, http.StatusOK)
@@ -126,7 +126,7 @@ func TestUnknowUser(t *testing.T) {
 	defer tests.PrepareTestEnv(t)()
 
 	session := loginUser(t, "user1")
-	token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeReadOrg)
+	token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeReadUser, auth_model.AccessTokenScopeReadOrganization)
 
 	req := NewRequest(t, "GET", fmt.Sprintf("/api/v1/users/unknow/orgs/org25/permissions?token=%s", token))
 	resp := MakeRequest(t, req, http.StatusNotFound)
@@ -140,7 +140,7 @@ func TestUnknowOrganization(t *testing.T) {
 	defer tests.PrepareTestEnv(t)()
 
 	session := loginUser(t, "user1")
-	token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeReadOrg)
+	token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeReadUser, auth_model.AccessTokenScopeReadOrganization)
 
 	req := NewRequest(t, "GET", fmt.Sprintf("/api/v1/users/user1/orgs/unknow/permissions?token=%s", token))
 	resp := MakeRequest(t, req, http.StatusNotFound)
diff --git a/tests/integration/api_user_orgs_test.go b/tests/integration/api_user_orgs_test.go
index 8f914b4875..323facaf48 100644
--- a/tests/integration/api_user_orgs_test.go
+++ b/tests/integration/api_user_orgs_test.go
@@ -70,7 +70,7 @@ func TestUserOrgs(t *testing.T) {
 func getUserOrgs(t *testing.T, userDoer, userCheck string) (orgs []*api.Organization) {
 	token := ""
 	if len(userDoer) != 0 {
-		token = getUserToken(t, userDoer, auth_model.AccessTokenScopeReadOrg)
+		token = getUserToken(t, userDoer, auth_model.AccessTokenScopeReadOrganization, auth_model.AccessTokenScopeReadUser)
 	}
 	urlStr := fmt.Sprintf("/api/v1/users/%s/orgs?token=%s", userCheck, token)
 	req := NewRequest(t, "GET", urlStr)
@@ -92,7 +92,7 @@ func TestMyOrgs(t *testing.T) {
 	MakeRequest(t, req, http.StatusUnauthorized)
 
 	normalUsername := "user2"
-	token := getUserToken(t, normalUsername, auth_model.AccessTokenScopeReadOrg)
+	token := getUserToken(t, normalUsername, auth_model.AccessTokenScopeReadOrganization, auth_model.AccessTokenScopeReadUser)
 	req = NewRequest(t, "GET", "/api/v1/user/orgs?token="+token)
 	resp := MakeRequest(t, req, http.StatusOK)
 	var orgs []*api.Organization
diff --git a/tests/integration/api_user_search_test.go b/tests/integration/api_user_search_test.go
index dc11281c46..be14d5a6b2 100644
--- a/tests/integration/api_user_search_test.go
+++ b/tests/integration/api_user_search_test.go
@@ -8,6 +8,7 @@ import (
 	"net/http"
 	"testing"
 
+	auth_model "code.gitea.io/gitea/models/auth"
 	"code.gitea.io/gitea/models/unittest"
 	user_model "code.gitea.io/gitea/models/user"
 	"code.gitea.io/gitea/modules/setting"
@@ -26,7 +27,7 @@ func TestAPIUserSearchLoggedIn(t *testing.T) {
 	defer tests.PrepareTestEnv(t)()
 	adminUsername := "user1"
 	session := loginUser(t, adminUsername)
-	token := getTokenForLoggedInUser(t, session)
+	token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeReadUser)
 	query := "user2"
 	req := NewRequestf(t, "GET", "/api/v1/users/search?token=%s&q=%s", token, query)
 	resp := MakeRequest(t, req, http.StatusOK)
@@ -65,7 +66,7 @@ func TestAPIUserSearchAdminLoggedInUserHidden(t *testing.T) {
 	defer tests.PrepareTestEnv(t)()
 	adminUsername := "user1"
 	session := loginUser(t, adminUsername)
-	token := getTokenForLoggedInUser(t, session)
+	token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeReadUser)
 	query := "user31"
 	req := NewRequestf(t, "GET", "/api/v1/users/search?token=%s&q=%s", token, query)
 	req.SetBasicAuth(token, "x-oauth-basic")
diff --git a/tests/integration/api_user_star_test.go b/tests/integration/api_user_star_test.go
index 6a486c19a8..15a555d17d 100644
--- a/tests/integration/api_user_star_test.go
+++ b/tests/integration/api_user_star_test.go
@@ -22,13 +22,13 @@ func TestAPIStar(t *testing.T) {
 	repo := "user2/repo1"
 
 	session := loginUser(t, user)
-	token := getTokenForLoggedInUser(t, session)
-	tokenWithRepoScope := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeRepo)
+	token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeReadUser)
+	tokenWithUserScope := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteUser, auth_model.AccessTokenScopeWriteRepository)
 
 	t.Run("Star", func(t *testing.T) {
 		defer tests.PrintCurrentTest(t)()
 
-		req := NewRequest(t, "PUT", fmt.Sprintf("/api/v1/user/starred/%s?token=%s", repo, tokenWithRepoScope))
+		req := NewRequest(t, "PUT", fmt.Sprintf("/api/v1/user/starred/%s?token=%s", repo, tokenWithUserScope))
 		MakeRequest(t, req, http.StatusNoContent)
 	})
 
@@ -49,7 +49,7 @@ func TestAPIStar(t *testing.T) {
 	t.Run("GetMyStarredRepos", func(t *testing.T) {
 		defer tests.PrintCurrentTest(t)()
 
-		req := NewRequest(t, "GET", fmt.Sprintf("/api/v1/user/starred?token=%s", tokenWithRepoScope))
+		req := NewRequest(t, "GET", fmt.Sprintf("/api/v1/user/starred?token=%s", tokenWithUserScope))
 		resp := MakeRequest(t, req, http.StatusOK)
 
 		assert.Equal(t, "1", resp.Header().Get("X-Total-Count"))
@@ -63,17 +63,17 @@ func TestAPIStar(t *testing.T) {
 	t.Run("IsStarring", func(t *testing.T) {
 		defer tests.PrintCurrentTest(t)()
 
-		req := NewRequest(t, "GET", fmt.Sprintf("/api/v1/user/starred/%s?token=%s", repo, tokenWithRepoScope))
+		req := NewRequest(t, "GET", fmt.Sprintf("/api/v1/user/starred/%s?token=%s", repo, tokenWithUserScope))
 		MakeRequest(t, req, http.StatusNoContent)
 
-		req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/user/starred/%s?token=%s", repo+"notexisting", tokenWithRepoScope))
+		req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/user/starred/%s?token=%s", repo+"notexisting", tokenWithUserScope))
 		MakeRequest(t, req, http.StatusNotFound)
 	})
 
 	t.Run("Unstar", func(t *testing.T) {
 		defer tests.PrintCurrentTest(t)()
 
-		req := NewRequest(t, "DELETE", fmt.Sprintf("/api/v1/user/starred/%s?token=%s", repo, tokenWithRepoScope))
+		req := NewRequest(t, "DELETE", fmt.Sprintf("/api/v1/user/starred/%s?token=%s", repo, tokenWithUserScope))
 		MakeRequest(t, req, http.StatusNoContent)
 	})
 }
diff --git a/tests/integration/api_user_watch_test.go b/tests/integration/api_user_watch_test.go
index 5702962573..c07fd288d1 100644
--- a/tests/integration/api_user_watch_test.go
+++ b/tests/integration/api_user_watch_test.go
@@ -22,8 +22,8 @@ func TestAPIWatch(t *testing.T) {
 	repo := "user2/repo1"
 
 	session := loginUser(t, user)
-	token := getTokenForLoggedInUser(t, session)
-	tokenWithRepoScope := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeRepo)
+	token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeReadUser)
+	tokenWithRepoScope := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeReadUser)
 
 	t.Run("Watch", func(t *testing.T) {
 		defer tests.PrintCurrentTest(t)()
diff --git a/tests/integration/api_wiki_test.go b/tests/integration/api_wiki_test.go
index 3f85074c8a..f598982555 100644
--- a/tests/integration/api_wiki_test.go
+++ b/tests/integration/api_wiki_test.go
@@ -180,7 +180,7 @@ func TestAPINewWikiPage(t *testing.T) {
 		defer tests.PrepareTestEnv(t)()
 		username := "user2"
 		session := loginUser(t, username)
-		token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeRepo)
+		token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository)
 
 		urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/wiki/new?token=%s", username, "repo1", token)
 
@@ -197,7 +197,7 @@ func TestAPIEditWikiPage(t *testing.T) {
 	defer tests.PrepareTestEnv(t)()
 	username := "user2"
 	session := loginUser(t, username)
-	token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeRepo)
+	token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository)
 
 	urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/wiki/page/Page-With-Spaced-Name?token=%s", username, "repo1", token)
 
diff --git a/tests/integration/dump_restore_test.go b/tests/integration/dump_restore_test.go
index 9ad795d53a..0b6707845b 100644
--- a/tests/integration/dump_restore_test.go
+++ b/tests/integration/dump_restore_test.go
@@ -51,7 +51,7 @@ func TestDumpRestore(t *testing.T) {
 		repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{Name: reponame})
 		repoOwner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
 		session := loginUser(t, repoOwner.Name)
-		token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeRepo)
+		token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteIssue, auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeReadMisc)
 
 		//
 		// Phase 1: dump repo1 from the Gitea instance to the filesystem
diff --git a/tests/integration/empty_repo_test.go b/tests/integration/empty_repo_test.go
index cfb2456223..f0022f4db3 100644
--- a/tests/integration/empty_repo_test.go
+++ b/tests/integration/empty_repo_test.go
@@ -117,7 +117,7 @@ func TestEmptyRepoAddFileByAPI(t *testing.T) {
 	assert.NoError(t, err)
 
 	session := loginUser(t, "user30")
-	token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeRepo)
+	token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository)
 
 	url := fmt.Sprintf("/api/v1/repos/user30/empty/contents/new-file.txt?token=%s", token)
 	req := NewRequestWithJSON(t, "POST", url, &api.CreateFileOptions{
diff --git a/tests/integration/eventsource_test.go b/tests/integration/eventsource_test.go
index 4fdb8cd6f5..734f4a6a0a 100644
--- a/tests/integration/eventsource_test.go
+++ b/tests/integration/eventsource_test.go
@@ -60,7 +60,7 @@ func TestEventSourceManagerRun(t *testing.T) {
 	thread5 := unittest.AssertExistsAndLoadBean(t, &activities_model.Notification{ID: 5})
 	assert.NoError(t, thread5.LoadAttributes(db.DefaultContext))
 	session := loginUser(t, user2.Name)
-	token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeNotification)
+	token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteNotification, auth_model.AccessTokenScopeWriteRepository)
 
 	var apiNL []api.NotificationThread
 
diff --git a/tests/integration/git_test.go b/tests/integration/git_test.go
index 8d5c0fc390..0c3a8616f0 100644
--- a/tests/integration/git_test.go
+++ b/tests/integration/git_test.go
@@ -44,11 +44,11 @@ func TestGit(t *testing.T) {
 
 func testGit(t *testing.T, u *url.URL) {
 	username := "user2"
-	baseAPITestContext := NewAPITestContext(t, username, "repo1", auth_model.AccessTokenScopeRepo, auth_model.AccessTokenScopeWritePublicKey, auth_model.AccessTokenScopeDeleteRepo)
+	baseAPITestContext := NewAPITestContext(t, username, "repo1", auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser)
 
 	u.Path = baseAPITestContext.GitPath()
 
-	forkedUserCtx := NewAPITestContext(t, "user4", "repo1", auth_model.AccessTokenScopeRepo)
+	forkedUserCtx := NewAPITestContext(t, "user4", "repo1", auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser)
 
 	t.Run("HTTP", func(t *testing.T) {
 		defer tests.PrintCurrentTest(t)()
@@ -359,7 +359,7 @@ func doBranchProtectPRMerge(baseCtx *APITestContext, dstPath string) func(t *tes
 		t.Run("CreateBranchProtected", doGitCreateBranch(dstPath, "protected"))
 		t.Run("PushProtectedBranch", doGitPushTestRepository(dstPath, "origin", "protected"))
 
-		ctx := NewAPITestContext(t, baseCtx.Username, baseCtx.Reponame, auth_model.AccessTokenScopeRepo)
+		ctx := NewAPITestContext(t, baseCtx.Username, baseCtx.Reponame, auth_model.AccessTokenScopeWriteRepository)
 		t.Run("ProtectProtectedBranchNoWhitelist", doProtectBranch(ctx, "protected", "", ""))
 		t.Run("GenerateCommit", func(t *testing.T) {
 			_, err := generateCommitWithNewData(littleSize, dstPath, "user2@example.com", "User Two", "branch-data-file-")
@@ -603,7 +603,7 @@ func doAutoPRMerge(baseCtx *APITestContext, dstPath string) func(t *testing.T) {
 	return func(t *testing.T) {
 		defer tests.PrintCurrentTest(t)()
 
-		ctx := NewAPITestContext(t, baseCtx.Username, baseCtx.Reponame, auth_model.AccessTokenScopeRepo)
+		ctx := NewAPITestContext(t, baseCtx.Username, baseCtx.Reponame, auth_model.AccessTokenScopeWriteRepository)
 
 		t.Run("CheckoutProtected", doGitCheckoutBranch(dstPath, "protected"))
 		t.Run("PullProtected", doGitPull(dstPath, "origin", "protected"))
diff --git a/tests/integration/gpg_git_test.go b/tests/integration/gpg_git_test.go
index 36095694b0..5b49c91c59 100644
--- a/tests/integration/gpg_git_test.go
+++ b/tests/integration/gpg_git_test.go
@@ -70,7 +70,7 @@ func TestGPGGit(t *testing.T) {
 
 		t.Run("Unsigned-Initial", func(t *testing.T) {
 			defer tests.PrintCurrentTest(t)()
-			testCtx := NewAPITestContext(t, username, "initial-unsigned", auth_model.AccessTokenScopeRepo)
+			testCtx := NewAPITestContext(t, username, "initial-unsigned", auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser)
 			t.Run("CreateRepository", doAPICreateRepository(testCtx, false))
 			t.Run("CheckMasterBranchUnsigned", doAPIGetBranch(testCtx, "master", func(t *testing.T, branch api.Branch) {
 				assert.NotNil(t, branch.Commit)
@@ -94,7 +94,7 @@ func TestGPGGit(t *testing.T) {
 
 		t.Run("Unsigned-Initial-CRUD-ParentSigned", func(t *testing.T) {
 			defer tests.PrintCurrentTest(t)()
-			testCtx := NewAPITestContext(t, username, "initial-unsigned", auth_model.AccessTokenScopeRepo)
+			testCtx := NewAPITestContext(t, username, "initial-unsigned", auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser)
 			t.Run("CreateCRUDFile-ParentSigned", crudActionCreateFile(
 				t, testCtx, user, "master", "parentsigned", "signed-parent.txt", func(t *testing.T, response api.FileResponse) {
 					assert.False(t, response.Verification.Verified)
@@ -111,7 +111,7 @@ func TestGPGGit(t *testing.T) {
 
 		t.Run("Unsigned-Initial-CRUD-Never", func(t *testing.T) {
 			defer tests.PrintCurrentTest(t)()
-			testCtx := NewAPITestContext(t, username, "initial-unsigned", auth_model.AccessTokenScopeRepo)
+			testCtx := NewAPITestContext(t, username, "initial-unsigned", auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser)
 			t.Run("CreateCRUDFile-Never", crudActionCreateFile(
 				t, testCtx, user, "parentsigned", "parentsigned-never", "unsigned-never2.txt", func(t *testing.T, response api.FileResponse) {
 					assert.False(t, response.Verification.Verified)
@@ -124,7 +124,7 @@ func TestGPGGit(t *testing.T) {
 
 		t.Run("Unsigned-Initial-CRUD-Always", func(t *testing.T) {
 			defer tests.PrintCurrentTest(t)()
-			testCtx := NewAPITestContext(t, username, "initial-unsigned", auth_model.AccessTokenScopeRepo)
+			testCtx := NewAPITestContext(t, username, "initial-unsigned", auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser)
 			t.Run("CreateCRUDFile-Always", crudActionCreateFile(
 				t, testCtx, user, "master", "always", "signed-always.txt", func(t *testing.T, response api.FileResponse) {
 					assert.NotNil(t, response.Verification)
@@ -161,7 +161,7 @@ func TestGPGGit(t *testing.T) {
 
 		t.Run("Unsigned-Initial-CRUD-ParentSigned", func(t *testing.T) {
 			defer tests.PrintCurrentTest(t)()
-			testCtx := NewAPITestContext(t, username, "initial-unsigned", auth_model.AccessTokenScopeRepo)
+			testCtx := NewAPITestContext(t, username, "initial-unsigned", auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser)
 			t.Run("CreateCRUDFile-Always-ParentSigned", crudActionCreateFile(
 				t, testCtx, user, "always", "always-parentsigned", "signed-always-parentsigned.txt", func(t *testing.T, response api.FileResponse) {
 					assert.NotNil(t, response.Verification)
@@ -184,7 +184,7 @@ func TestGPGGit(t *testing.T) {
 
 		t.Run("AlwaysSign-Initial", func(t *testing.T) {
 			defer tests.PrintCurrentTest(t)()
-			testCtx := NewAPITestContext(t, username, "initial-always", auth_model.AccessTokenScopeRepo)
+			testCtx := NewAPITestContext(t, username, "initial-always", auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser)
 			t.Run("CreateRepository", doAPICreateRepository(testCtx, false))
 			t.Run("CheckMasterBranchSigned", doAPIGetBranch(testCtx, "master", func(t *testing.T, branch api.Branch) {
 				assert.NotNil(t, branch.Commit)
@@ -212,7 +212,7 @@ func TestGPGGit(t *testing.T) {
 
 		t.Run("AlwaysSign-Initial-CRUD-Never", func(t *testing.T) {
 			defer tests.PrintCurrentTest(t)()
-			testCtx := NewAPITestContext(t, username, "initial-always-never", auth_model.AccessTokenScopeRepo)
+			testCtx := NewAPITestContext(t, username, "initial-always-never", auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser)
 			t.Run("CreateRepository", doAPICreateRepository(testCtx, false))
 			t.Run("CreateCRUDFile-Never", crudActionCreateFile(
 				t, testCtx, user, "master", "never", "unsigned-never.txt", func(t *testing.T, response api.FileResponse) {
@@ -225,7 +225,7 @@ func TestGPGGit(t *testing.T) {
 		u.Path = baseAPITestContext.GitPath()
 		t.Run("AlwaysSign-Initial-CRUD-ParentSigned-On-Always", func(t *testing.T) {
 			defer tests.PrintCurrentTest(t)()
-			testCtx := NewAPITestContext(t, username, "initial-always-parent", auth_model.AccessTokenScopeRepo)
+			testCtx := NewAPITestContext(t, username, "initial-always-parent", auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser)
 			t.Run("CreateRepository", doAPICreateRepository(testCtx, false))
 			t.Run("CreateCRUDFile-ParentSigned", crudActionCreateFile(
 				t, testCtx, user, "master", "parentsigned", "signed-parent.txt", func(t *testing.T, response api.FileResponse) {
@@ -244,7 +244,7 @@ func TestGPGGit(t *testing.T) {
 
 		t.Run("AlwaysSign-Initial-CRUD-Always", func(t *testing.T) {
 			defer tests.PrintCurrentTest(t)()
-			testCtx := NewAPITestContext(t, username, "initial-always-always", auth_model.AccessTokenScopeRepo)
+			testCtx := NewAPITestContext(t, username, "initial-always-always", auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser)
 			t.Run("CreateRepository", doAPICreateRepository(testCtx, false))
 			t.Run("CreateCRUDFile-Always", crudActionCreateFile(
 				t, testCtx, user, "master", "always", "signed-always.txt", func(t *testing.T, response api.FileResponse) {
@@ -264,7 +264,7 @@ func TestGPGGit(t *testing.T) {
 
 		t.Run("UnsignedMerging", func(t *testing.T) {
 			defer tests.PrintCurrentTest(t)()
-			testCtx := NewAPITestContext(t, username, "initial-unsigned", auth_model.AccessTokenScopeRepo)
+			testCtx := NewAPITestContext(t, username, "initial-unsigned", auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser)
 			var err error
 			t.Run("CreatePullRequest", func(t *testing.T) {
 				pr, err = doAPICreatePullRequest(testCtx, testCtx.Username, testCtx.Reponame, "master", "never2")(t)
@@ -285,7 +285,7 @@ func TestGPGGit(t *testing.T) {
 
 		t.Run("BaseSignedMerging", func(t *testing.T) {
 			defer tests.PrintCurrentTest(t)()
-			testCtx := NewAPITestContext(t, username, "initial-unsigned", auth_model.AccessTokenScopeRepo)
+			testCtx := NewAPITestContext(t, username, "initial-unsigned", auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser)
 			var err error
 			t.Run("CreatePullRequest", func(t *testing.T) {
 				pr, err = doAPICreatePullRequest(testCtx, testCtx.Username, testCtx.Reponame, "master", "parentsigned2")(t)
@@ -306,7 +306,7 @@ func TestGPGGit(t *testing.T) {
 
 		t.Run("CommitsSignedMerging", func(t *testing.T) {
 			defer tests.PrintCurrentTest(t)()
-			testCtx := NewAPITestContext(t, username, "initial-unsigned", auth_model.AccessTokenScopeRepo)
+			testCtx := NewAPITestContext(t, username, "initial-unsigned", auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser)
 			var err error
 			t.Run("CreatePullRequest", func(t *testing.T) {
 				pr, err = doAPICreatePullRequest(testCtx, testCtx.Username, testCtx.Reponame, "master", "always-parentsigned")(t)
diff --git a/tests/integration/lfs_getobject_test.go b/tests/integration/lfs_getobject_test.go
index ba236d355f..fe070c62d5 100644
--- a/tests/integration/lfs_getobject_test.go
+++ b/tests/integration/lfs_getobject_test.go
@@ -41,13 +41,13 @@ func storeObjectInRepo(t *testing.T, repositoryID int64, content *[]byte) string
 	return pointer.Oid
 }
 
-func storeAndGetLfsToken(t *testing.T, ts auth.AccessTokenScope, content *[]byte, extraHeader *http.Header, expectedStatus int) *httptest.ResponseRecorder {
+func storeAndGetLfsToken(t *testing.T, content *[]byte, extraHeader *http.Header, expectedStatus int, ts ...auth.AccessTokenScope) *httptest.ResponseRecorder {
 	repo, err := repo_model.GetRepositoryByOwnerAndName(db.DefaultContext, "user2", "repo1")
 	assert.NoError(t, err)
 	oid := storeObjectInRepo(t, repo.ID, content)
 	defer git_model.RemoveLFSMetaObjectByOid(db.DefaultContext, repo.ID, oid)
 
-	token := getUserToken(t, "user2", ts)
+	token := getUserToken(t, "user2", ts...)
 
 	// Request OID
 	req := NewRequest(t, "GET", "/user2/repo1.git/info/lfs/objects/"+oid+"/test")
@@ -119,7 +119,7 @@ func TestGetLFSSmallToken(t *testing.T) {
 	defer tests.PrepareTestEnv(t)()
 	content := []byte("A very small file\n")
 
-	resp := storeAndGetLfsToken(t, auth.AccessTokenScopePublicRepo, &content, nil, http.StatusOK)
+	resp := storeAndGetLfsToken(t, &content, nil, http.StatusOK, auth.AccessTokenScopePublicOnly, auth.AccessTokenScopeReadRepository)
 	checkResponseTestContentEncoding(t, &content, resp, false)
 }
 
@@ -127,7 +127,7 @@ func TestGetLFSSmallTokenFail(t *testing.T) {
 	defer tests.PrepareTestEnv(t)()
 	content := []byte("A very small file\n")
 
-	storeAndGetLfsToken(t, auth.AccessTokenScopeNotification, &content, nil, http.StatusForbidden)
+	storeAndGetLfsToken(t, &content, nil, http.StatusForbidden, auth.AccessTokenScopeReadNotification)
 }
 
 func TestGetLFSLarge(t *testing.T) {
diff --git a/tests/integration/migrate_test.go b/tests/integration/migrate_test.go
index a925493d7c..f25329f66b 100644
--- a/tests/integration/migrate_test.go
+++ b/tests/integration/migrate_test.go
@@ -67,7 +67,7 @@ func TestMigrateGiteaForm(t *testing.T) {
 		repoName := "repo1"
 		repoOwner := unittest.AssertExistsAndLoadBean(t, &user_model.User{Name: ownerName})
 		session := loginUser(t, ownerName)
-		token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeRepo)
+		token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeReadMisc)
 
 		// Step 0: verify the repo is available
 		req := NewRequestf(t, "GET", fmt.Sprintf("/%s/%s", ownerName, repoName))
diff --git a/tests/integration/org_count_test.go b/tests/integration/org_count_test.go
index 8f850a170f..e46b83169b 100644
--- a/tests/integration/org_count_test.go
+++ b/tests/integration/org_count_test.go
@@ -25,7 +25,7 @@ func testOrgCounts(t *testing.T, u *url.URL) {
 	orgOwner := "user2"
 	orgName := "testOrg"
 	orgCollaborator := "user4"
-	ctx := NewAPITestContext(t, orgOwner, "repo1", auth_model.AccessTokenScopeAdminOrg)
+	ctx := NewAPITestContext(t, orgOwner, "repo1", auth_model.AccessTokenScopeWriteOrganization)
 
 	var ownerCountRepos map[string]int
 	var collabCountRepos map[string]int
diff --git a/tests/integration/org_test.go b/tests/integration/org_test.go
index bfa6380e8a..64d93e4083 100644
--- a/tests/integration/org_test.go
+++ b/tests/integration/org_test.go
@@ -159,7 +159,7 @@ func TestOrgRestrictedUser(t *testing.T) {
 
 	// Therefore create a read-only team
 	adminSession := loginUser(t, "user1")
-	token := getTokenForLoggedInUser(t, adminSession, auth_model.AccessTokenScopeAdminOrg)
+	token := getTokenForLoggedInUser(t, adminSession, auth_model.AccessTokenScopeWriteOrganization)
 
 	teamToCreate := &api.CreateTeamOption{
 		Name:                    "codereader",
diff --git a/tests/integration/privateactivity_test.go b/tests/integration/privateactivity_test.go
index 6e1377ae1f..5d3291bfe3 100644
--- a/tests/integration/privateactivity_test.go
+++ b/tests/integration/privateactivity_test.go
@@ -34,7 +34,7 @@ func testPrivateActivityDoSomethingForActionEntries(t *testing.T) {
 	owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repoBefore.OwnerID})
 
 	session := loginUser(t, privateActivityTestUser)
-	token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeRepo)
+	token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteIssue)
 	urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/issues?state=all&token=%s", owner.Name, repoBefore.Name, token)
 	req := NewRequestWithJSON(t, "POST", urlStr, &api.CreateIssueOption{
 		Body:  "test",
@@ -125,7 +125,7 @@ func testPrivateActivityHelperHasHeatmapContentFromPublic(t *testing.T) bool {
 }
 
 func testPrivateActivityHelperHasHeatmapContentFromSession(t *testing.T, session *TestSession) bool {
-	token := getTokenForLoggedInUser(t, session)
+	token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeReadUser)
 
 	req := NewRequestf(t, "GET", "/api/v1/users/%s/heatmap?token=%s", privateActivityTestUser, token)
 	resp := session.MakeRequest(t, req, http.StatusOK)
diff --git a/tests/integration/pull_merge_test.go b/tests/integration/pull_merge_test.go
index f6a36f60af..8890347c36 100644
--- a/tests/integration/pull_merge_test.go
+++ b/tests/integration/pull_merge_test.go
@@ -218,7 +218,7 @@ func TestCantMergeConflict(t *testing.T) {
 		testEditFileToNewBranch(t, session, "user1", "repo1", "master", "base", "README.md", "Hello, World (Edited Twice)\n")
 
 		// Use API to create a conflicting pr
-		token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeRepo)
+		token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository)
 		req := NewRequestWithJSON(t, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/%s/pulls?token=%s", "user1", "repo1", token), &api.CreatePullRequestOption{
 			Head:  "conflict",
 			Base:  "base",
@@ -326,7 +326,7 @@ func TestCantMergeUnrelated(t *testing.T) {
 		testEditFileToNewBranch(t, session, "user1", "repo1", "master", "conflict", "README.md", "Hello, World (Edited Once)\n")
 
 		// Use API to create a conflicting pr
-		token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeRepo)
+		token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository)
 		req := NewRequestWithJSON(t, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/%s/pulls?token=%s", "user1", "repo1", token), &api.CreatePullRequestOption{
 			Head:  "unrelated",
 			Base:  "base",
diff --git a/tests/integration/pull_status_test.go b/tests/integration/pull_status_test.go
index 736d1ee4f0..0bdb80ecbf 100644
--- a/tests/integration/pull_status_test.go
+++ b/tests/integration/pull_status_test.go
@@ -64,7 +64,7 @@ func TestPullCreate_CommitStatus(t *testing.T) {
 			api.CommitStatusWarning: "gitea-exclamation",
 		}
 
-		testCtx := NewAPITestContext(t, "user1", "repo1", auth_model.AccessTokenScopeRepo)
+		testCtx := NewAPITestContext(t, "user1", "repo1", auth_model.AccessTokenScopeWriteRepository)
 
 		// Update commit status, and check if icon is updated as well
 		for _, status := range statusList {
diff --git a/tests/integration/pull_update_test.go b/tests/integration/pull_update_test.go
index b94731002f..fa56cec485 100644
--- a/tests/integration/pull_update_test.go
+++ b/tests/integration/pull_update_test.go
@@ -39,7 +39,7 @@ func TestAPIPullUpdate(t *testing.T) {
 		assert.NoError(t, pr.LoadIssue(db.DefaultContext))
 
 		session := loginUser(t, "user2")
-		token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeRepo)
+		token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository)
 		req := NewRequestf(t, "POST", "/api/v1/repos/%s/%s/pulls/%d/update?token="+token, pr.BaseRepo.OwnerName, pr.BaseRepo.Name, pr.Issue.Index)
 		session.MakeRequest(t, req, http.StatusOK)
 
@@ -67,7 +67,7 @@ func TestAPIPullUpdateByRebase(t *testing.T) {
 		assert.NoError(t, pr.LoadIssue(db.DefaultContext))
 
 		session := loginUser(t, "user2")
-		token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeRepo)
+		token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository)
 		req := NewRequestf(t, "POST", "/api/v1/repos/%s/%s/pulls/%d/update?style=rebase&token="+token, pr.BaseRepo.OwnerName, pr.BaseRepo.Name, pr.Issue.Index)
 		session.MakeRequest(t, req, http.StatusOK)
 
diff --git a/tests/integration/repo_commits_test.go b/tests/integration/repo_commits_test.go
index 0cfd21485d..99927f1929 100644
--- a/tests/integration/repo_commits_test.go
+++ b/tests/integration/repo_commits_test.go
@@ -52,7 +52,7 @@ func doTestRepoCommitWithStatus(t *testing.T, state string, classes ...string) {
 	assert.NotEmpty(t, commitURL)
 
 	// Call API to add status for commit
-	ctx := NewAPITestContext(t, "user2", "repo1", auth_model.AccessTokenScopeRepo)
+	ctx := NewAPITestContext(t, "user2", "repo1", auth_model.AccessTokenScopeWriteRepository)
 	t.Run("CreateStatus", doAPICreateCommitStatus(ctx, path.Base(commitURL), api.CreateStatusOption{
 		State:       api.CommitStatusState(state),
 		TargetURL:   "http://test.ci/",
@@ -157,7 +157,7 @@ func TestRepoCommitsStatusParallel(t *testing.T) {
 		wg.Add(1)
 		go func(parentT *testing.T, i int) {
 			parentT.Run(fmt.Sprintf("ParallelCreateStatus_%d", i), func(t *testing.T) {
-				ctx := NewAPITestContext(t, "user2", "repo1", auth_model.AccessTokenScopeRepoStatus)
+				ctx := NewAPITestContext(t, "user2", "repo1", auth_model.AccessTokenScopeWriteRepository)
 				runBody := doAPICreateCommitStatus(ctx, path.Base(commitURL), api.CreateStatusOption{
 					State:       api.CommitStatusPending,
 					TargetURL:   "http://test.ci/",
@@ -188,7 +188,7 @@ func TestRepoCommitsStatusMultiple(t *testing.T) {
 	assert.NotEmpty(t, commitURL)
 
 	// Call API to add status for commit
-	ctx := NewAPITestContext(t, "user2", "repo1", auth_model.AccessTokenScopeRepo)
+	ctx := NewAPITestContext(t, "user2", "repo1", auth_model.AccessTokenScopeWriteRepository)
 	t.Run("CreateStatus", doAPICreateCommitStatus(ctx, path.Base(commitURL), api.CreateStatusOption{
 		State:       api.CommitStatusSuccess,
 		TargetURL:   "http://test.ci/",
diff --git a/tests/integration/ssh_key_test.go b/tests/integration/ssh_key_test.go
index 1e9dc264a6..eb3a3e926a 100644
--- a/tests/integration/ssh_key_test.go
+++ b/tests/integration/ssh_key_test.go
@@ -48,8 +48,8 @@ func TestPushDeployKeyOnEmptyRepo(t *testing.T) {
 
 func testPushDeployKeyOnEmptyRepo(t *testing.T, u *url.URL) {
 	// OK login
-	ctx := NewAPITestContext(t, "user2", "deploy-key-empty-repo-1", auth_model.AccessTokenScopeRepo)
-	ctxWithDeleteRepo := NewAPITestContext(t, "user2", "deploy-key-empty-repo-1", auth_model.AccessTokenScopeRepo, auth_model.AccessTokenScopeDeleteRepo)
+	ctx := NewAPITestContext(t, "user2", "deploy-key-empty-repo-1", auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser)
+	ctxWithDeleteRepo := NewAPITestContext(t, "user2", "deploy-key-empty-repo-1", auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser)
 
 	keyname := fmt.Sprintf("%s-push", ctx.Reponame)
 	u.Path = ctx.GitPath()
@@ -92,8 +92,8 @@ func testKeyOnlyOneType(t *testing.T, u *url.URL) {
 	keyname := fmt.Sprintf("%s-push", reponame)
 
 	// OK login
-	ctx := NewAPITestContext(t, username, reponame, auth_model.AccessTokenScopeRepo, auth_model.AccessTokenScopeAdminPublicKey)
-	ctxWithDeleteRepo := NewAPITestContext(t, username, reponame, auth_model.AccessTokenScopeRepo, auth_model.AccessTokenScopeAdminPublicKey, auth_model.AccessTokenScopeDeleteRepo)
+	ctx := NewAPITestContext(t, username, reponame, auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser)
+	ctxWithDeleteRepo := NewAPITestContext(t, username, reponame, auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser)
 
 	otherCtx := ctx
 	otherCtx.Reponame = "ssh-key-test-repo-2"
diff --git a/tests/integration/user_test.go b/tests/integration/user_test.go
index 65cba1dee3..3e4d967686 100644
--- a/tests/integration/user_test.go
+++ b/tests/integration/user_test.go
@@ -166,7 +166,7 @@ Note: This user hasn't uploaded any GPG keys.
 	// Import key
 	// User1 <user1@example.com>
 	session := loginUser(t, "user1")
-	token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteGPGKey)
+	token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteUser)
 	testCreateGPGKey(t, session.MakeRequest, token, http.StatusCreated, `-----BEGIN PGP PUBLIC KEY BLOCK-----
 
 mQENBFyy/VUBCADJ7zbM20Z1RWmFoVgp5WkQfI2rU1Vj9cQHes9i42wVLLtcbPeo
diff --git a/web_src/css/helpers.css b/web_src/css/helpers.css
index cfd2151100..14ef64b252 100644
--- a/web_src/css/helpers.css
+++ b/web_src/css/helpers.css
@@ -64,6 +64,7 @@ Gitea's private styles use `g-` prefix.
 .gt-break-all { word-break: break-all !important; }
 .gt-content-center { align-content: center !important; }
 .gt-cursor-default { cursor: default !important; }
+.gt-cursor-pointer { cursor: pointer !important; }
 .gt-invisible { visibility: hidden !important; }
 .gt-items-start { align-items: flex-start !important; }
 .gt-pointer-events-none { pointer-events: none !important; }
diff --git a/web_src/js/components/ScopedAccessTokenSelector.vue b/web_src/js/components/ScopedAccessTokenSelector.vue
new file mode 100644
index 0000000000..769f3262b4
--- /dev/null
+++ b/web_src/js/components/ScopedAccessTokenSelector.vue
@@ -0,0 +1,138 @@
+<template>
+  <div class="scoped-access-token-category">
+    <div class="field gt-pl-2">
+      <label class="checkbox-label">
+        <input
+          ref="category"
+          v-model="categorySelected"
+          class="scope-checkbox scoped-access-token-input"
+          type="checkbox"
+          name="scope"
+          :value="'write:' + category"
+          @input="onCategoryInput"
+        >
+        {{ category }}
+      </label>
+    </div>
+    <div class="field gt-pl-4">
+      <div class="inline field">
+        <label class="checkbox-label">
+          <input
+            ref="read"
+            v-model="readSelected"
+            :disabled="disableIndividual || writeSelected"
+            class="scope-checkbox scoped-access-token-input"
+            type="checkbox"
+            name="scope"
+            :value="'read:' + category"
+            @input="onIndividualInput"
+          >
+          read:{{ category }}
+        </label>
+      </div>
+      <div class="inline field">
+        <label class="checkbox-label">
+          <input
+            ref="write"
+            v-model="writeSelected"
+            :disabled="disableIndividual"
+            class="scope-checkbox scoped-access-token-input"
+            type="checkbox"
+            name="scope"
+            :value="'write:' + category"
+            @input="onIndividualInput"
+          >
+          write:{{ category }}
+        </label>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script>
+import {createApp} from 'vue';
+import {showElem} from '../utils/dom.js';
+
+const sfc = {
+  props: {
+    category: {
+      type: String,
+      required: true,
+    },
+  },
+
+  data: () => ({
+    categorySelected: false,
+    disableIndividual: false,
+    readSelected: false,
+    writeSelected: false,
+  }),
+
+  methods: {
+    /**
+     * When entire category is toggled
+     * @param {Event} e
+     */
+    onCategoryInput(e) {
+      e.preventDefault();
+      this.disableIndividual = this.$refs.category.checked;
+      this.writeSelected = this.$refs.category.checked;
+      this.readSelected = this.$refs.category.checked;
+    },
+
+    /**
+     * When an individual level of category is toggled
+     * @param {Event} e
+     */
+    onIndividualInput(e) {
+      e.preventDefault();
+      if (this.$refs.write.checked) {
+        this.readSelected = true;
+      }
+      this.categorySelected = this.$refs.write.checked;
+    },
+  }
+};
+
+export default sfc;
+
+/**
+ * Initialize category toggle sections
+ */
+export function initScopedAccessTokenCategories() {
+  for (const el of document.getElementsByTagName('scoped-access-token-category')) {
+    const category = el.getAttribute('category');
+    createApp(sfc, {
+      category,
+    }).mount(el);
+  }
+
+  document.getElementById('scoped-access-submit')?.addEventListener('click', (e) => {
+    e.preventDefault();
+    // check that at least one scope has been selected
+    for (const el of document.getElementsByClassName('scoped-access-token-input')) {
+      if (el.checked) {
+        document.getElementById('scoped-access-form').submit();
+      }
+    }
+    // no scopes selected, show validation error
+    showElem(document.getElementById('scoped-access-warning'));
+  });
+}
+
+</script>
+
+<style scoped>
+.scoped-access-token-category {
+  padding-top: 10px;
+  padding-bottom: 10px;
+}
+
+.checkbox-label {
+  cursor: pointer;
+}
+
+.scope-checkbox {
+  margin: 4px 5px 0 0;
+}
+</style>
diff --git a/web_src/js/index.js b/web_src/js/index.js
index 3d8a7fc325..0c786f96fb 100644
--- a/web_src/js/index.js
+++ b/web_src/js/index.js
@@ -2,6 +2,7 @@
 import './bootstrap.js';
 
 import {initRepoActivityTopAuthorsChart} from './components/RepoActivityTopAuthors.vue';
+import {initScopedAccessTokenCategories} from './components/ScopedAccessTokenSelector.vue';
 import {initDashboardRepoList} from './components/DashboardRepoList.vue';
 
 import {initGlobalCopyToClipboardListener} from './features/clipboard.js';
@@ -177,4 +178,5 @@ onDomReady(() => {
   initUserSettings();
   initRepoDiffView();
   initPdfViewer();
+  initScopedAccessTokenCategories();
 });