Support scoped access tokens (#20908)

This PR adds the support for scopes of access tokens, mimicking the
design of GitHub OAuth scopes.

The changes of the core logic are in `models/auth` that `AccessToken`
struct will have a `Scope` field. The normalized (no duplication of
scope), comma-separated scope string will be stored in `access_token`
table in the database.
In `services/auth`, the scope will be stored in context, which will be
used by `reqToken` middleware in API calls. Only OAuth2 tokens will have
granular token scopes, while others like BasicAuth will default to scope
`all`.
A large amount of work happens in `routers/api/v1/api.go` and the
corresponding `tests/integration` tests, that is adding necessary scopes
to each of the API calls as they fit.


- [x] Add `Scope` field to `AccessToken`
- [x] Add access control to all API endpoints
- [x] Update frontend & backend for when creating tokens
- [x] Add a database migration for `scope` column (enable 'all' access
to past tokens)

I'm aiming to complete it before Gitea 1.19 release.

Fixes #4300
This commit is contained in:
Chongyi Zheng 2023-01-17 16:46:03 -05:00 committed by GitHub
parent db2286bbb6
commit de484e86bc
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
79 changed files with 1221 additions and 449 deletions

View file

@ -21,6 +21,7 @@ import (
"testing"
"time"
"code.gitea.io/gitea/models/auth"
"code.gitea.io/gitea/models/unittest"
"code.gitea.io/gitea/modules/graceful"
"code.gitea.io/gitea/modules/json"
@ -217,8 +218,8 @@ func emptyTestSession(t testing.TB) *TestSession {
return &TestSession{jar: jar}
}
func getUserToken(t testing.TB, userName string) string {
return getTokenForLoggedInUser(t, loginUser(t, userName))
func getUserToken(t testing.TB, userName string, scope ...auth.AccessTokenScope) string {
return getTokenForLoggedInUser(t, loginUser(t, userName), scope...)
}
func loginUser(t testing.TB, userName string) *TestSession {
@ -256,7 +257,10 @@ func loginUserWithPassword(t testing.TB, userName, password string) *TestSession
// token has to be unique this counter take care of
var tokenCounter int64
func getTokenForLoggedInUser(t testing.TB, session *TestSession) string {
// getTokenForLoggedInUser returns a token for a logged in user.
// The scope is an optional list of snake_case strings like the frontend form fields,
// but without the "scope_" prefix.
func getTokenForLoggedInUser(t testing.TB, session *TestSession, scopes ...auth.AccessTokenScope) string {
t.Helper()
var token string
req := NewRequest(t, "GET", "/user/settings/applications")
@ -274,10 +278,13 @@ func getTokenForLoggedInUser(t testing.TB, session *TestSession) string {
csrf = doc.GetCSRF()
}
assert.NotEmpty(t, csrf)
req = NewRequestWithValues(t, "POST", "/user/settings/applications", map[string]string{
"_csrf": csrf,
"name": fmt.Sprintf("api-testing-token-%d", atomic.AddInt64(&tokenCounter, 1)),
})
urlValues := url.Values{}
urlValues.Add("_csrf", csrf)
urlValues.Add("name", fmt.Sprintf("api-testing-token-%d", atomic.AddInt64(&tokenCounter, 1)))
for _, scope := range scopes {
urlValues.Add("scope", string(scope))
}
req = NewRequestWithURLValues(t, "POST", "/user/settings/applications", urlValues)
resp = session.MakeRequest(t, req, http.StatusSeeOther)
// Log the flash values on failure
@ -317,6 +324,11 @@ func NewRequestWithValues(t testing.TB, method, urlStr string, values map[string
for key, value := range values {
urlValues[key] = []string{value}
}
return NewRequestWithURLValues(t, method, urlStr, urlValues)
}
func NewRequestWithURLValues(t testing.TB, method, urlStr string, urlValues url.Values) *http.Request {
t.Helper()
req := NewRequestWithBody(t, method, urlStr, bytes.NewBufferString(urlValues.Encode()))
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
return req