mirror of
https://codeberg.org/forgejo/forgejo.git
synced 2026-05-12 22:10:25 +00:00
feat: remove admin-level permissions from repo-specific & public-only access tokens (#11468)
This PR is part of a series (#11311). If the user authenticating to an API call is a Forgejo site administrator, or a Forgejo repo administrator, a wide variety of permission and ownership checks in the API are either bypassed, or are bypassable. If a user has created an access token with restricted resources, I understand the intent of the user is to create a token which has a layer of risk reduction in the event that the token is lost/leaked to an attacker. For this reason, it makes sense to me that restricted scope access tokens shouldn't inherit the owner's administrator access. My intent is that repo-specific access tokens [will only be able to access specific authorization scopes](https://codeberg.org/forgejo/design/issues/50#issuecomment-11093951), probably: `repository:read`, `repository:write`, `issue:read`, `issue:write`, (`organization:read` / `user:read` maybe). This means that *most* admin access is not intended to be affected by this because repo-specific access tokens won't have, for example, `admin:write` scope. However, administrative access still grants elevated permissions in some areas that are relevant to these scopes, and need to be restricted: - The `?sudo=otheruser` query parameter allows site administrators to impersonate other users in the API. - Repository management rules are different for a site administrator, allowing them to create repos for another user, create repos in another organization, migrate a repository to an arbitrary owner, and transfer a repository to a prviate organization. - Administrators have access to extra data through some APIs which would be in scope: the detailed configuration of branch protection rules, the some details of repository deploy keys (which repo, and which scope -- seems odd), (user:read -- user SSH keys, activity feeds of private users, user profiles of private users, user webhook configurations). - Pull request reviews have additional perms for repo administrators, including the ability to dismiss PR reviews, delete PR reviews, and view draft PR reviews. - Repo admins and site admins can comment on locked issues, and related to comments can edit or delete other user's comments and attachments. - Repo admins can manage and view logged time on behalf of other users. A handful of these permissions may make sense for repo-specific access tokens, but most of them clearly exceed the risk that would be expected from creating a limited scope access token. I'd generally prefer to take a restrictive approach, and we can relax it if real-world use-cases come in -- users will have a workaround of creating an access token without repo-specific restrictions if they are blocked from needed access. **Breaking:** The administration restrictions introduced in this PR affect both repo-specific access tokens, and existing public-only access tokens. ## Checklist The [contributor guide](https://forgejo.org/docs/next/contributor/) contains information that will be helpful to first time contributors. There also are a few [conditions for merging Pull Requests in Forgejo repositories](https://codeberg.org/forgejo/governance/src/branch/main/PullRequestsAgreement.md). You are also welcome to join the [Forgejo development chatroom](https://matrix.to/#/#forgejo-development:matrix.org). ### Tests for Go changes (can be removed for JavaScript changes) - I added test coverage for Go changes... - [x] in their respective `*_test.go` for unit tests. - [x] in the `tests/integration` directory if it involves interactions with a live Forgejo server. - I ran... - [x] `make pr-go` before pushing ### Documentation - [ ] I created a pull request [to the documentation](https://codeberg.org/forgejo/docs) to explain to Forgejo users how to use this change. - [x] I did not document these changes and I do not expect someone else to do it. ### Release notes - [x] This change will be noticed by a Forgejo user or admin (feature, bug fix, performance, etc.). I suggest to include a release note for this change. - Although repo-specific access tokens are not yet exposed to end users, the breaking changes to public-only tokens will be visible to users and require release notes. - [ ] This change is not visible to a Forgejo user or admin (refactor, dependency upgrade, etc.). I think there is no need to add a release note for this change. Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/11468 Reviewed-by: Andreas Ahlenstorf <aahlenst@noreply.codeberg.org> Co-authored-by: Mathieu Fenniak <mathieu@fenniak.net> Co-committed-by: Mathieu Fenniak <mathieu@fenniak.net>
This commit is contained in:
parent
0dd594baca
commit
99984dac4d
29 changed files with 483 additions and 349 deletions
|
|
@ -28,7 +28,7 @@ func LogAndProcessError(ctx *context.Context, status int, obj any, cb func(strin
|
|||
// LogAndProcessError is always wrapped in a `apiError` call, so we need to skip two frames
|
||||
log.ErrorWithSkip(2, message)
|
||||
|
||||
if setting.IsProd && (ctx.Doer == nil || !ctx.Doer.IsAdmin) {
|
||||
if setting.IsProd && (ctx.Doer == nil || !ctx.IsUserSiteAdmin()) {
|
||||
message = ""
|
||||
}
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
// Copyright 2026 The Forgejo Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
package v1
|
||||
package shared
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
|
@ -35,7 +35,8 @@ func Middlewares() (stack []any) {
|
|||
|
||||
checkDeprecatedAuthMethods,
|
||||
// Get user from session if logged in.
|
||||
apiAuth(buildAuthGroup()),
|
||||
apiAuthentication(buildAuthGroup()),
|
||||
apiAuthorization,
|
||||
verifyAuthWithOptions(&common.VerifyOptions{
|
||||
SignInRequired: setting.Service.RequireSignInView,
|
||||
}),
|
||||
|
|
@ -55,7 +56,7 @@ func buildAuthGroup() *auth.Group {
|
|||
return group
|
||||
}
|
||||
|
||||
func apiAuth(authMethod auth.Method) func(*context.APIContext) {
|
||||
func apiAuthentication(authMethod auth.Method) func(*context.APIContext) {
|
||||
return func(ctx *context.APIContext) {
|
||||
ar, err := common.AuthShared(ctx.Base, nil, authMethod)
|
||||
if err != nil {
|
||||
|
|
@ -65,8 +66,30 @@ func apiAuth(authMethod auth.Method) func(*context.APIContext) {
|
|||
ctx.Doer = ar.Doer
|
||||
ctx.IsSigned = ar.Doer != nil
|
||||
ctx.IsBasicAuth = ar.IsBasicAuth
|
||||
if ctx.Reducer == nil {
|
||||
// Ensure ctx.Reducer isn't nil, but has no impact:
|
||||
}
|
||||
}
|
||||
|
||||
func apiAuthorization(ctx *context.APIContext) {
|
||||
scope, scopeExists := ctx.Data["ApiTokenScope"].(auth_model.AccessTokenScope)
|
||||
if scopeExists {
|
||||
publicOnly, err := scope.PublicOnly()
|
||||
if err != nil {
|
||||
ctx.Error(http.StatusForbidden, "tokenRequiresScope", "parsing public resource scope failed: "+err.Error())
|
||||
return
|
||||
}
|
||||
ctx.PublicOnly = publicOnly
|
||||
}
|
||||
|
||||
reducer, reducerExists := ctx.Data["ApiTokenReducer"].(authz.AuthorizationReducer)
|
||||
if reducerExists {
|
||||
ctx.Reducer = reducer
|
||||
} else {
|
||||
// No "ApiTokenReducer" will be populated if the auth method wasn't an PAT. In this case, we populate
|
||||
// `ctx.Reducer` so no nil checks are needed, and we respect the scope `PublicOnly()` so that it it's safe to
|
||||
// just rely on `ctx.Reducer` to account for public-only access:
|
||||
if ctx.PublicOnly {
|
||||
ctx.Reducer = &authz.PublicReposAuthorizationReducer{}
|
||||
} else {
|
||||
ctx.Reducer = &authz.AllAccessAuthorizationReducer{}
|
||||
}
|
||||
}
|
||||
|
|
@ -143,7 +166,7 @@ func verifyAuthWithOptions(options *common.VerifyOptions) func(ctx *context.APIC
|
|||
}
|
||||
|
||||
if options.AdminRequired {
|
||||
if !ctx.Doer.IsAdmin {
|
||||
if !ctx.IsUserSiteAdmin() {
|
||||
ctx.JSON(http.StatusForbidden, map[string]string{
|
||||
"message": "You have no permission to request for this.",
|
||||
})
|
||||
|
|
|
|||
|
|
@ -5,21 +5,33 @@ package shared
|
|||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
auth_model "forgejo.org/models/auth"
|
||||
"forgejo.org/models/db"
|
||||
"forgejo.org/models/unittest"
|
||||
"forgejo.org/modules/json"
|
||||
"forgejo.org/modules/jwtx"
|
||||
"forgejo.org/modules/test"
|
||||
"forgejo.org/modules/web"
|
||||
"forgejo.org/routers/common"
|
||||
"forgejo.org/services/auth/source/oauth2"
|
||||
"forgejo.org/services/authz"
|
||||
"forgejo.org/services/context"
|
||||
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestReducer(t *testing.T) {
|
||||
defer unittest.OverrideFixtures("routers/api/shared/TestReducer")()
|
||||
require.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
makeRecorder := func() *httptest.ResponseRecorder {
|
||||
buff := bytes.NewBufferString("")
|
||||
recorder := httptest.NewRecorder()
|
||||
|
|
@ -32,9 +44,11 @@ func TestReducer(t *testing.T) {
|
|||
r.Use(Middlewares()...)
|
||||
|
||||
type ReducerInfo struct {
|
||||
IsSigned bool
|
||||
IsNil bool
|
||||
IsAllAccess bool
|
||||
IsSigned bool
|
||||
IsNil bool
|
||||
IsAllAccess bool
|
||||
IsPublicAccess bool
|
||||
IsSpecificAccess bool
|
||||
}
|
||||
|
||||
r.Get("/api/test", func(ctx *context.APIContext) {
|
||||
|
|
@ -46,23 +60,206 @@ func TestReducer(t *testing.T) {
|
|||
_, isAllAccess := ctx.Reducer.(*authz.AllAccessAuthorizationReducer)
|
||||
retval.IsAllAccess = isAllAccess
|
||||
|
||||
_, isPublicAccess := ctx.Reducer.(*authz.PublicReposAuthorizationReducer)
|
||||
retval.IsPublicAccess = isPublicAccess
|
||||
|
||||
_, isSpecificAccess := ctx.Reducer.(*authz.SpecificReposAuthorizationReducer)
|
||||
retval.IsSpecificAccess = isSpecificAccess
|
||||
|
||||
ctx.JSON(http.StatusOK, retval)
|
||||
})
|
||||
|
||||
// shared middleware ensures that `APIContext.Reducer` is not nil, and so that's the only test required in this
|
||||
// package's scope -- an anonymous request and `Reducer` is not nil:
|
||||
t.Run("anonymous", func(t *testing.T) {
|
||||
recorder := makeRecorder()
|
||||
req, err := http.NewRequest("GET", "http://localhost:8000/api/test", nil)
|
||||
t.Run("Basic Auth w/ PAT", func(t *testing.T) {
|
||||
t.Run("unrestricted access token", func(t *testing.T) {
|
||||
recorder := makeRecorder()
|
||||
req, err := http.NewRequest("GET", "http://localhost:8000/api/test", nil)
|
||||
req.SetBasicAuth("token", "4a0c970da8bf58408a8c22264b2ac1ff47dadcce")
|
||||
require.NoError(t, err)
|
||||
r.ServeHTTP(recorder, req)
|
||||
assert.Equal(t, http.StatusOK, recorder.Code)
|
||||
|
||||
var reducerInfo ReducerInfo
|
||||
require.NoError(t, json.Unmarshal(recorder.Body.Bytes(), &reducerInfo))
|
||||
|
||||
assert.True(t, reducerInfo.IsSigned)
|
||||
assert.False(t, reducerInfo.IsNil)
|
||||
assert.True(t, reducerInfo.IsAllAccess)
|
||||
assert.False(t, reducerInfo.IsPublicAccess)
|
||||
assert.False(t, reducerInfo.IsSpecificAccess)
|
||||
})
|
||||
|
||||
t.Run("public-only access token", func(t *testing.T) {
|
||||
recorder := makeRecorder()
|
||||
req, err := http.NewRequest("GET", "http://localhost:8000/api/test", nil)
|
||||
req.SetBasicAuth("token", "83909b5b978acc5620ae0c7b0e55b548da2e26b5")
|
||||
require.NoError(t, err)
|
||||
r.ServeHTTP(recorder, req)
|
||||
assert.Equal(t, http.StatusOK, recorder.Code)
|
||||
|
||||
var reducerInfo ReducerInfo
|
||||
require.NoError(t, json.Unmarshal(recorder.Body.Bytes(), &reducerInfo))
|
||||
|
||||
assert.True(t, reducerInfo.IsSigned)
|
||||
assert.False(t, reducerInfo.IsNil)
|
||||
assert.False(t, reducerInfo.IsAllAccess)
|
||||
assert.True(t, reducerInfo.IsPublicAccess)
|
||||
assert.False(t, reducerInfo.IsSpecificAccess)
|
||||
})
|
||||
|
||||
t.Run("specific-repo access token", func(t *testing.T) {
|
||||
recorder := makeRecorder()
|
||||
req, err := http.NewRequest("GET", "http://localhost:8000/api/test", nil)
|
||||
req.SetBasicAuth("token", "46088605ec804b43ebd15cef1b3f210c31b066dd")
|
||||
require.NoError(t, err)
|
||||
r.ServeHTTP(recorder, req)
|
||||
assert.Equal(t, http.StatusOK, recorder.Code)
|
||||
|
||||
var reducerInfo ReducerInfo
|
||||
require.NoError(t, json.Unmarshal(recorder.Body.Bytes(), &reducerInfo))
|
||||
|
||||
assert.True(t, reducerInfo.IsSigned)
|
||||
assert.False(t, reducerInfo.IsNil)
|
||||
assert.False(t, reducerInfo.IsAllAccess)
|
||||
assert.False(t, reducerInfo.IsPublicAccess)
|
||||
assert.True(t, reducerInfo.IsSpecificAccess)
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("Token Auth", func(t *testing.T) {
|
||||
t.Run("unrestricted access token", func(t *testing.T) {
|
||||
recorder := makeRecorder()
|
||||
req, err := http.NewRequest("GET", "http://localhost:8000/api/test", nil)
|
||||
req.Header.Set("Authorization", "token 4a0c970da8bf58408a8c22264b2ac1ff47dadcce")
|
||||
require.NoError(t, err)
|
||||
r.ServeHTTP(recorder, req)
|
||||
assert.Equal(t, http.StatusOK, recorder.Code)
|
||||
|
||||
var reducerInfo ReducerInfo
|
||||
require.NoError(t, json.Unmarshal(recorder.Body.Bytes(), &reducerInfo))
|
||||
|
||||
assert.True(t, reducerInfo.IsSigned)
|
||||
assert.False(t, reducerInfo.IsNil)
|
||||
assert.True(t, reducerInfo.IsAllAccess)
|
||||
assert.False(t, reducerInfo.IsPublicAccess)
|
||||
assert.False(t, reducerInfo.IsSpecificAccess)
|
||||
})
|
||||
|
||||
t.Run("public-only access token", func(t *testing.T) {
|
||||
recorder := makeRecorder()
|
||||
req, err := http.NewRequest("GET", "http://localhost:8000/api/test", nil)
|
||||
req.Header.Set("Authorization", "token 83909b5b978acc5620ae0c7b0e55b548da2e26b5")
|
||||
require.NoError(t, err)
|
||||
r.ServeHTTP(recorder, req)
|
||||
assert.Equal(t, http.StatusOK, recorder.Code)
|
||||
|
||||
var reducerInfo ReducerInfo
|
||||
require.NoError(t, json.Unmarshal(recorder.Body.Bytes(), &reducerInfo))
|
||||
|
||||
assert.True(t, reducerInfo.IsSigned)
|
||||
assert.False(t, reducerInfo.IsNil)
|
||||
assert.False(t, reducerInfo.IsAllAccess)
|
||||
assert.True(t, reducerInfo.IsPublicAccess)
|
||||
assert.False(t, reducerInfo.IsSpecificAccess)
|
||||
})
|
||||
|
||||
t.Run("specific-repo access token", func(t *testing.T) {
|
||||
recorder := makeRecorder()
|
||||
req, err := http.NewRequest("GET", "http://localhost:8000/api/test", nil)
|
||||
req.Header.Set("Authorization", "token 46088605ec804b43ebd15cef1b3f210c31b066dd")
|
||||
require.NoError(t, err)
|
||||
r.ServeHTTP(recorder, req)
|
||||
assert.Equal(t, http.StatusOK, recorder.Code)
|
||||
|
||||
var reducerInfo ReducerInfo
|
||||
require.NoError(t, json.Unmarshal(recorder.Body.Bytes(), &reducerInfo))
|
||||
|
||||
assert.True(t, reducerInfo.IsSigned)
|
||||
assert.False(t, reducerInfo.IsNil)
|
||||
assert.False(t, reducerInfo.IsAllAccess)
|
||||
assert.False(t, reducerInfo.IsPublicAccess)
|
||||
assert.True(t, reducerInfo.IsSpecificAccess)
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("OAuth", func(t *testing.T) {
|
||||
signingKey, err := jwtx.CreateSigningKey("HS256", make([]byte, 32))
|
||||
require.NoError(t, err)
|
||||
r.ServeHTTP(recorder, req)
|
||||
assert.Equal(t, http.StatusOK, recorder.Code)
|
||||
defer test.MockVariableValue(&oauth2.DefaultSigningKey, signingKey)()
|
||||
|
||||
var reducerInfo ReducerInfo
|
||||
require.NoError(t, json.Unmarshal(recorder.Body.Bytes(), &reducerInfo))
|
||||
t.Run("unrestricted grant", func(t *testing.T) {
|
||||
grant := &auth_model.OAuth2Grant{
|
||||
UserID: 2,
|
||||
ApplicationID: 100, // fake, but required here for unique constraint
|
||||
Scope: "write:repository",
|
||||
}
|
||||
_, err = db.GetEngine(t.Context()).Insert(grant)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.False(t, reducerInfo.IsSigned)
|
||||
assert.False(t, reducerInfo.IsNil)
|
||||
assert.True(t, reducerInfo.IsAllAccess)
|
||||
token := oauth2.Token{
|
||||
GrantID: grant.ID,
|
||||
Type: oauth2.TypeAccessToken,
|
||||
Counter: 100,
|
||||
RegisteredClaims: jwt.RegisteredClaims{
|
||||
IssuedAt: jwt.NewNumericDate(time.Now().Add(-1 * time.Hour)),
|
||||
ExpiresAt: jwt.NewNumericDate(time.Now().Add(1 * time.Hour)),
|
||||
},
|
||||
}
|
||||
signed, err := token.SignToken(oauth2.DefaultSigningKey)
|
||||
require.NoError(t, err)
|
||||
|
||||
recorder := makeRecorder()
|
||||
req, err := http.NewRequest("GET", "http://localhost:8000/api/test", nil)
|
||||
req.Header.Set("Authorization", fmt.Sprintf("bearer %s", signed))
|
||||
require.NoError(t, err)
|
||||
r.ServeHTTP(recorder, req)
|
||||
assert.Equal(t, http.StatusOK, recorder.Code)
|
||||
|
||||
var reducerInfo ReducerInfo
|
||||
require.NoError(t, json.Unmarshal(recorder.Body.Bytes(), &reducerInfo))
|
||||
|
||||
assert.True(t, reducerInfo.IsSigned)
|
||||
assert.False(t, reducerInfo.IsNil)
|
||||
assert.True(t, reducerInfo.IsAllAccess)
|
||||
assert.False(t, reducerInfo.IsPublicAccess)
|
||||
assert.False(t, reducerInfo.IsSpecificAccess)
|
||||
})
|
||||
|
||||
t.Run("public-only grant", func(t *testing.T) {
|
||||
grant := &auth_model.OAuth2Grant{
|
||||
UserID: 2,
|
||||
ApplicationID: 101, // fake, but required here for unique constraint
|
||||
Scope: "write:repository public-only",
|
||||
}
|
||||
_, err = db.GetEngine(t.Context()).Insert(grant)
|
||||
require.NoError(t, err)
|
||||
|
||||
token := oauth2.Token{
|
||||
GrantID: grant.ID,
|
||||
Type: oauth2.TypeAccessToken,
|
||||
Counter: 100,
|
||||
RegisteredClaims: jwt.RegisteredClaims{
|
||||
IssuedAt: jwt.NewNumericDate(time.Now().Add(-1 * time.Hour)),
|
||||
ExpiresAt: jwt.NewNumericDate(time.Now().Add(1 * time.Hour)),
|
||||
},
|
||||
}
|
||||
signed, err := token.SignToken(oauth2.DefaultSigningKey)
|
||||
require.NoError(t, err)
|
||||
|
||||
recorder := makeRecorder()
|
||||
req, err := http.NewRequest("GET", "http://localhost:8000/api/test", nil)
|
||||
req.Header.Set("Authorization", fmt.Sprintf("bearer %s", signed))
|
||||
require.NoError(t, err)
|
||||
r.ServeHTTP(recorder, req)
|
||||
assert.Equal(t, http.StatusOK, recorder.Code)
|
||||
|
||||
var reducerInfo ReducerInfo
|
||||
require.NoError(t, json.Unmarshal(recorder.Body.Bytes(), &reducerInfo))
|
||||
|
||||
assert.True(t, reducerInfo.IsSigned)
|
||||
assert.False(t, reducerInfo.IsNil)
|
||||
assert.False(t, reducerInfo.IsAllAccess)
|
||||
assert.True(t, reducerInfo.IsPublicAccess)
|
||||
assert.False(t, reducerInfo.IsSpecificAccess)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -86,7 +86,6 @@ import (
|
|||
"forgejo.org/routers/api/v1/user"
|
||||
"forgejo.org/services/actions"
|
||||
"forgejo.org/services/auth"
|
||||
"forgejo.org/services/authz"
|
||||
"forgejo.org/services/context"
|
||||
"forgejo.org/services/forms"
|
||||
redirect_service "forgejo.org/services/redirect"
|
||||
|
|
@ -105,7 +104,7 @@ func sudo() func(ctx *context.APIContext) {
|
|||
}
|
||||
|
||||
if len(sudo) > 0 {
|
||||
if ctx.IsSigned && ctx.Doer.IsAdmin {
|
||||
if ctx.IsSigned && ctx.IsUserSiteAdmin() {
|
||||
user, err := user_model.GetUserByName(ctx, sudo)
|
||||
if err != nil {
|
||||
if user_model.IsErrUserNotExist(err) {
|
||||
|
|
@ -356,30 +355,6 @@ func tokenRequiresScopes(requiredScopeCategories ...auth_model.AccessTokenScopeC
|
|||
}
|
||||
|
||||
ctx.Data["requiredScopeCategories"] = 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
|
||||
}
|
||||
|
||||
// assign to true so that those searching should only filter public repositories/users/organizations
|
||||
ctx.PublicOnly = publicOnly
|
||||
|
||||
reducer, ok := ctx.Data["ApiTokenReducer"].(authz.AuthorizationReducer)
|
||||
if ok {
|
||||
ctx.Reducer = reducer
|
||||
} else {
|
||||
// No "ApiTokenReducer" will be populated if the auth method wasn't an PAT. In this case, we populate
|
||||
// `ctx.Reducer` so no nil checks are needed, and we respect the scope `PublicOnly()` so that it it's safe
|
||||
// to just rely on `ctx.Reducer` to account for public-only access:
|
||||
if ctx.PublicOnly {
|
||||
ctx.Reducer = &authz.PublicReposAuthorizationReducer{}
|
||||
} else {
|
||||
ctx.Reducer = &authz.AllAccessAuthorizationReducer{}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -825,7 +800,7 @@ func individualPermsChecker(ctx *context.APIContext) {
|
|||
if ctx.ContextUser.IsIndividual() {
|
||||
switch ctx.ContextUser.Visibility {
|
||||
case api.VisibleTypePrivate:
|
||||
if ctx.Doer == nil || (ctx.ContextUser.ID != ctx.Doer.ID && !ctx.Doer.IsAdmin) {
|
||||
if ctx.Doer == nil || (ctx.ContextUser.ID != ctx.Doer.ID && !ctx.IsUserSiteAdmin()) {
|
||||
ctx.NotFound("Visit Project", nil)
|
||||
return
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,266 +0,0 @@
|
|||
// Copyright 2026 The Forgejo Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
package v1
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
auth_model "forgejo.org/models/auth"
|
||||
"forgejo.org/models/db"
|
||||
"forgejo.org/models/unittest"
|
||||
"forgejo.org/modules/json"
|
||||
"forgejo.org/modules/jwtx"
|
||||
"forgejo.org/modules/test"
|
||||
"forgejo.org/modules/web"
|
||||
"forgejo.org/routers/api/shared"
|
||||
"forgejo.org/routers/common"
|
||||
"forgejo.org/services/auth/source/oauth2"
|
||||
"forgejo.org/services/authz"
|
||||
"forgejo.org/services/context"
|
||||
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestTokenRequiresScopes(t *testing.T) {
|
||||
defer unittest.OverrideFixtures("routers/api/v1/TestTokenRequiresScopes")()
|
||||
require.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
makeRecorder := func() *httptest.ResponseRecorder {
|
||||
buff := bytes.NewBufferString("")
|
||||
recorder := httptest.NewRecorder()
|
||||
recorder.Body = buff
|
||||
return recorder
|
||||
}
|
||||
|
||||
r := web.NewRoute()
|
||||
r.Use(common.ProtocolMiddlewares()...)
|
||||
r.Use(shared.Middlewares()...)
|
||||
|
||||
type ReducerInfo struct {
|
||||
IsSigned bool
|
||||
IsNil bool
|
||||
IsAllAccess bool
|
||||
IsPublicAccess bool
|
||||
IsSpecificAccess bool
|
||||
}
|
||||
|
||||
r.Get("/api/test", tokenRequiresScopes(auth_model.AccessTokenScopeCategoryRepository), func(ctx *context.APIContext) {
|
||||
retval := ReducerInfo{
|
||||
IsSigned: ctx.IsSigned,
|
||||
IsNil: ctx.Reducer == nil,
|
||||
}
|
||||
|
||||
_, isAllAccess := ctx.Reducer.(*authz.AllAccessAuthorizationReducer)
|
||||
retval.IsAllAccess = isAllAccess
|
||||
|
||||
_, isPublicAccess := ctx.Reducer.(*authz.PublicReposAuthorizationReducer)
|
||||
retval.IsPublicAccess = isPublicAccess
|
||||
|
||||
_, isSpecificAccess := ctx.Reducer.(*authz.SpecificReposAuthorizationReducer)
|
||||
retval.IsSpecificAccess = isSpecificAccess
|
||||
|
||||
ctx.JSON(http.StatusOK, retval)
|
||||
})
|
||||
|
||||
t.Run("Basic Auth w/ PAT", func(t *testing.T) {
|
||||
t.Run("unrestricted access token", func(t *testing.T) {
|
||||
recorder := makeRecorder()
|
||||
req, err := http.NewRequest("GET", "http://localhost:8000/api/test", nil)
|
||||
req.SetBasicAuth("token", "4a0c970da8bf58408a8c22264b2ac1ff47dadcce")
|
||||
require.NoError(t, err)
|
||||
r.ServeHTTP(recorder, req)
|
||||
assert.Equal(t, http.StatusOK, recorder.Code)
|
||||
|
||||
var reducerInfo ReducerInfo
|
||||
require.NoError(t, json.Unmarshal(recorder.Body.Bytes(), &reducerInfo))
|
||||
|
||||
assert.True(t, reducerInfo.IsSigned)
|
||||
assert.False(t, reducerInfo.IsNil)
|
||||
assert.True(t, reducerInfo.IsAllAccess)
|
||||
assert.False(t, reducerInfo.IsPublicAccess)
|
||||
assert.False(t, reducerInfo.IsSpecificAccess)
|
||||
})
|
||||
|
||||
t.Run("public-only access token", func(t *testing.T) {
|
||||
recorder := makeRecorder()
|
||||
req, err := http.NewRequest("GET", "http://localhost:8000/api/test", nil)
|
||||
req.SetBasicAuth("token", "83909b5b978acc5620ae0c7b0e55b548da2e26b5")
|
||||
require.NoError(t, err)
|
||||
r.ServeHTTP(recorder, req)
|
||||
assert.Equal(t, http.StatusOK, recorder.Code)
|
||||
|
||||
var reducerInfo ReducerInfo
|
||||
require.NoError(t, json.Unmarshal(recorder.Body.Bytes(), &reducerInfo))
|
||||
|
||||
assert.True(t, reducerInfo.IsSigned)
|
||||
assert.False(t, reducerInfo.IsNil)
|
||||
assert.False(t, reducerInfo.IsAllAccess)
|
||||
assert.True(t, reducerInfo.IsPublicAccess)
|
||||
assert.False(t, reducerInfo.IsSpecificAccess)
|
||||
})
|
||||
|
||||
t.Run("specific-repo access token", func(t *testing.T) {
|
||||
recorder := makeRecorder()
|
||||
req, err := http.NewRequest("GET", "http://localhost:8000/api/test", nil)
|
||||
req.SetBasicAuth("token", "46088605ec804b43ebd15cef1b3f210c31b066dd")
|
||||
require.NoError(t, err)
|
||||
r.ServeHTTP(recorder, req)
|
||||
assert.Equal(t, http.StatusOK, recorder.Code)
|
||||
|
||||
var reducerInfo ReducerInfo
|
||||
require.NoError(t, json.Unmarshal(recorder.Body.Bytes(), &reducerInfo))
|
||||
|
||||
assert.True(t, reducerInfo.IsSigned)
|
||||
assert.False(t, reducerInfo.IsNil)
|
||||
assert.False(t, reducerInfo.IsAllAccess)
|
||||
assert.False(t, reducerInfo.IsPublicAccess)
|
||||
assert.True(t, reducerInfo.IsSpecificAccess)
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("Token Auth", func(t *testing.T) {
|
||||
t.Run("unrestricted access token", func(t *testing.T) {
|
||||
recorder := makeRecorder()
|
||||
req, err := http.NewRequest("GET", "http://localhost:8000/api/test", nil)
|
||||
req.Header.Set("Authorization", "token 4a0c970da8bf58408a8c22264b2ac1ff47dadcce")
|
||||
require.NoError(t, err)
|
||||
r.ServeHTTP(recorder, req)
|
||||
assert.Equal(t, http.StatusOK, recorder.Code)
|
||||
|
||||
var reducerInfo ReducerInfo
|
||||
require.NoError(t, json.Unmarshal(recorder.Body.Bytes(), &reducerInfo))
|
||||
|
||||
assert.True(t, reducerInfo.IsSigned)
|
||||
assert.False(t, reducerInfo.IsNil)
|
||||
assert.True(t, reducerInfo.IsAllAccess)
|
||||
assert.False(t, reducerInfo.IsPublicAccess)
|
||||
assert.False(t, reducerInfo.IsSpecificAccess)
|
||||
})
|
||||
|
||||
t.Run("public-only access token", func(t *testing.T) {
|
||||
recorder := makeRecorder()
|
||||
req, err := http.NewRequest("GET", "http://localhost:8000/api/test", nil)
|
||||
req.Header.Set("Authorization", "token 83909b5b978acc5620ae0c7b0e55b548da2e26b5")
|
||||
require.NoError(t, err)
|
||||
r.ServeHTTP(recorder, req)
|
||||
assert.Equal(t, http.StatusOK, recorder.Code)
|
||||
|
||||
var reducerInfo ReducerInfo
|
||||
require.NoError(t, json.Unmarshal(recorder.Body.Bytes(), &reducerInfo))
|
||||
|
||||
assert.True(t, reducerInfo.IsSigned)
|
||||
assert.False(t, reducerInfo.IsNil)
|
||||
assert.False(t, reducerInfo.IsAllAccess)
|
||||
assert.True(t, reducerInfo.IsPublicAccess)
|
||||
assert.False(t, reducerInfo.IsSpecificAccess)
|
||||
})
|
||||
|
||||
t.Run("specific-repo access token", func(t *testing.T) {
|
||||
recorder := makeRecorder()
|
||||
req, err := http.NewRequest("GET", "http://localhost:8000/api/test", nil)
|
||||
req.Header.Set("Authorization", "token 46088605ec804b43ebd15cef1b3f210c31b066dd")
|
||||
require.NoError(t, err)
|
||||
r.ServeHTTP(recorder, req)
|
||||
assert.Equal(t, http.StatusOK, recorder.Code)
|
||||
|
||||
var reducerInfo ReducerInfo
|
||||
require.NoError(t, json.Unmarshal(recorder.Body.Bytes(), &reducerInfo))
|
||||
|
||||
assert.True(t, reducerInfo.IsSigned)
|
||||
assert.False(t, reducerInfo.IsNil)
|
||||
assert.False(t, reducerInfo.IsAllAccess)
|
||||
assert.False(t, reducerInfo.IsPublicAccess)
|
||||
assert.True(t, reducerInfo.IsSpecificAccess)
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("OAuth", func(t *testing.T) {
|
||||
signingKey, err := jwtx.CreateSigningKey("HS256", make([]byte, 32))
|
||||
require.NoError(t, err)
|
||||
defer test.MockVariableValue(&oauth2.DefaultSigningKey, signingKey)()
|
||||
|
||||
t.Run("unrestricted grant", func(t *testing.T) {
|
||||
grant := &auth_model.OAuth2Grant{
|
||||
UserID: 2,
|
||||
ApplicationID: 100, // fake, but required here for unique constraint
|
||||
Scope: "write:repository",
|
||||
}
|
||||
_, err = db.GetEngine(t.Context()).Insert(grant)
|
||||
require.NoError(t, err)
|
||||
|
||||
token := oauth2.Token{
|
||||
GrantID: grant.ID,
|
||||
Type: oauth2.TypeAccessToken,
|
||||
Counter: 100,
|
||||
RegisteredClaims: jwt.RegisteredClaims{
|
||||
IssuedAt: jwt.NewNumericDate(time.Now().Add(-1 * time.Hour)),
|
||||
ExpiresAt: jwt.NewNumericDate(time.Now().Add(1 * time.Hour)),
|
||||
},
|
||||
}
|
||||
signed, err := token.SignToken(oauth2.DefaultSigningKey)
|
||||
require.NoError(t, err)
|
||||
|
||||
recorder := makeRecorder()
|
||||
req, err := http.NewRequest("GET", "http://localhost:8000/api/test", nil)
|
||||
req.Header.Set("Authorization", fmt.Sprintf("bearer %s", signed))
|
||||
require.NoError(t, err)
|
||||
r.ServeHTTP(recorder, req)
|
||||
assert.Equal(t, http.StatusOK, recorder.Code)
|
||||
|
||||
var reducerInfo ReducerInfo
|
||||
require.NoError(t, json.Unmarshal(recorder.Body.Bytes(), &reducerInfo))
|
||||
|
||||
assert.True(t, reducerInfo.IsSigned)
|
||||
assert.False(t, reducerInfo.IsNil)
|
||||
assert.True(t, reducerInfo.IsAllAccess)
|
||||
assert.False(t, reducerInfo.IsPublicAccess)
|
||||
assert.False(t, reducerInfo.IsSpecificAccess)
|
||||
})
|
||||
|
||||
t.Run("public-only grant", func(t *testing.T) {
|
||||
grant := &auth_model.OAuth2Grant{
|
||||
UserID: 2,
|
||||
ApplicationID: 101, // fake, but required here for unique constraint
|
||||
Scope: "write:repository public-only",
|
||||
}
|
||||
_, err = db.GetEngine(t.Context()).Insert(grant)
|
||||
require.NoError(t, err)
|
||||
|
||||
token := oauth2.Token{
|
||||
GrantID: grant.ID,
|
||||
Type: oauth2.TypeAccessToken,
|
||||
Counter: 100,
|
||||
RegisteredClaims: jwt.RegisteredClaims{
|
||||
IssuedAt: jwt.NewNumericDate(time.Now().Add(-1 * time.Hour)),
|
||||
ExpiresAt: jwt.NewNumericDate(time.Now().Add(1 * time.Hour)),
|
||||
},
|
||||
}
|
||||
signed, err := token.SignToken(oauth2.DefaultSigningKey)
|
||||
require.NoError(t, err)
|
||||
|
||||
recorder := makeRecorder()
|
||||
req, err := http.NewRequest("GET", "http://localhost:8000/api/test", nil)
|
||||
req.Header.Set("Authorization", fmt.Sprintf("bearer %s", signed))
|
||||
require.NoError(t, err)
|
||||
r.ServeHTTP(recorder, req)
|
||||
assert.Equal(t, http.StatusOK, recorder.Code)
|
||||
|
||||
var reducerInfo ReducerInfo
|
||||
require.NoError(t, json.Unmarshal(recorder.Body.Bytes(), &reducerInfo))
|
||||
|
||||
assert.True(t, reducerInfo.IsSigned)
|
||||
assert.False(t, reducerInfo.IsNil)
|
||||
assert.False(t, reducerInfo.IsAllAccess)
|
||||
assert.True(t, reducerInfo.IsPublicAccess)
|
||||
assert.False(t, reducerInfo.IsSpecificAccess)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
|
@ -112,7 +112,7 @@ func getThread(ctx *context.APIContext) *activities_model.Notification {
|
|||
}
|
||||
return nil
|
||||
}
|
||||
if n.UserID != ctx.Doer.ID && !ctx.Doer.IsAdmin {
|
||||
if n.UserID != ctx.Doer.ID && !ctx.IsUserSiteAdmin() {
|
||||
ctx.Error(http.StatusForbidden, "GetNotificationByID", fmt.Errorf("only user itself and admin are allowed to read/change this thread %d", n.ID))
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -152,7 +152,7 @@ func IsMember(ctx *context.APIContext) {
|
|||
if err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "IsOrgMember", err)
|
||||
return
|
||||
} else if userIsMember || ctx.Doer.IsAdmin {
|
||||
} else if userIsMember || ctx.IsUserSiteAdmin() {
|
||||
userToCheckIsMember, err := ctx.Org.Organization.IsOrgMember(ctx, userToCheck.ID)
|
||||
if err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "IsOrgMember", err)
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ import (
|
|||
|
||||
func listUserOrgs(ctx *context.APIContext, u *user_model.User) {
|
||||
listOptions := utils.GetListOptions(ctx)
|
||||
showPrivate := ctx.IsSigned && (ctx.Doer.IsAdmin || ctx.Doer.ID == u.ID)
|
||||
showPrivate := ctx.IsSigned && (ctx.IsUserSiteAdmin() || ctx.Doer.ID == u.ID)
|
||||
|
||||
opts := organization.FindOrgOptions{
|
||||
ListOptions: listOptions,
|
||||
|
|
@ -199,7 +199,7 @@ func GetAll(ctx *context.APIContext) {
|
|||
vMode := []api.VisibleType{api.VisibleTypePublic}
|
||||
if ctx.IsSigned && !ctx.PublicOnly {
|
||||
vMode = append(vMode, api.VisibleTypeLimited)
|
||||
if ctx.Doer.IsAdmin {
|
||||
if ctx.IsUserSiteAdmin() {
|
||||
vMode = append(vMode, api.VisibleTypePrivate)
|
||||
}
|
||||
}
|
||||
|
|
@ -483,7 +483,7 @@ func ListOrgActivityFeeds(ctx *context.APIContext) {
|
|||
|
||||
includePrivate := false
|
||||
if ctx.IsSigned {
|
||||
if ctx.Doer.IsAdmin {
|
||||
if ctx.IsUserSiteAdmin() {
|
||||
includePrivate = true
|
||||
} else {
|
||||
org := organization.OrgFromUser(ctx.ContextUser)
|
||||
|
|
|
|||
|
|
@ -405,7 +405,7 @@ func GetTeamMembers(ctx *context.APIContext) {
|
|||
if err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "IsOrganizationMember", err)
|
||||
return
|
||||
} else if !isMember && !ctx.Doer.IsAdmin {
|
||||
} else if !isMember && !ctx.IsUserSiteAdmin() {
|
||||
ctx.NotFound()
|
||||
return
|
||||
}
|
||||
|
|
@ -822,7 +822,7 @@ func SearchTeam(ctx *context.APIContext) {
|
|||
}
|
||||
|
||||
// Only admin is allowed to search for all teams
|
||||
if !ctx.Doer.IsAdmin {
|
||||
if !ctx.IsUserSiteAdmin() {
|
||||
opts.UserID = ctx.Doer.ID
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -79,7 +79,7 @@ func GetBranch(ctx *context.APIContext) {
|
|||
return
|
||||
}
|
||||
|
||||
br, err := convert.ToBranch(ctx, ctx.Repo.Repository, branch.Name, c, branchProtection, ctx.Doer, ctx.Repo.IsAdmin())
|
||||
br, err := convert.ToBranch(ctx, ctx.Repo.Repository, branch.Name, c, branchProtection, ctx.Doer, ctx.IsUserRepoAdmin())
|
||||
if err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "convert.ToBranch", err)
|
||||
return
|
||||
|
|
@ -283,7 +283,7 @@ func CreateBranch(ctx *context.APIContext) {
|
|||
return
|
||||
}
|
||||
|
||||
br, err := convert.ToBranch(ctx, ctx.Repo.Repository, branch.Name, commit, branchProtection, ctx.Doer, ctx.Repo.IsAdmin())
|
||||
br, err := convert.ToBranch(ctx, ctx.Repo.Repository, branch.Name, commit, branchProtection, ctx.Doer, ctx.IsUserRepoAdmin())
|
||||
if err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "convert.ToBranch", err)
|
||||
return
|
||||
|
|
@ -378,7 +378,7 @@ func ListBranches(ctx *context.APIContext) {
|
|||
}
|
||||
|
||||
branchProtection := rules.GetFirstMatched(branches[i].Name)
|
||||
apiBranch, err := convert.ToBranch(ctx, ctx.Repo.Repository, branches[i].Name, c, branchProtection, ctx.Doer, ctx.Repo.IsAdmin())
|
||||
apiBranch, err := convert.ToBranch(ctx, ctx.Repo.Repository, branches[i].Name, c, branchProtection, ctx.Doer, ctx.IsUserRepoAdmin())
|
||||
if err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "convert.ToBranch", err)
|
||||
return
|
||||
|
|
|
|||
|
|
@ -418,7 +418,7 @@ func CreateIssueComment(ctx *context.APIContext) {
|
|||
return
|
||||
}
|
||||
|
||||
if issue.IsLocked && !ctx.Repo.CanWriteIssuesOrPulls(issue.IsPull) && !ctx.Doer.IsAdmin {
|
||||
if issue.IsLocked && !ctx.Repo.CanWriteIssuesOrPulls(issue.IsPull) && !ctx.IsUserSiteAdmin() {
|
||||
ctx.Error(http.StatusForbidden, "CreateIssueComment", errors.New(ctx.Locale.TrString("repo.issues.comment_on_locked")))
|
||||
return
|
||||
}
|
||||
|
|
|
|||
|
|
@ -127,7 +127,7 @@ func setIssueSubscription(ctx *context.APIContext, watch bool) {
|
|||
}
|
||||
|
||||
// only admin and user for itself can change subscription
|
||||
if user.ID != ctx.Doer.ID && !ctx.Doer.IsAdmin {
|
||||
if user.ID != ctx.Doer.ID && !ctx.IsUserSiteAdmin() {
|
||||
ctx.Error(http.StatusForbidden, "User", fmt.Errorf("%s is not permitted to change subscriptions for %s", ctx.Doer.Name, user.Name))
|
||||
return
|
||||
}
|
||||
|
|
|
|||
|
|
@ -109,7 +109,7 @@ func ListTrackedTimes(ctx *context.APIContext) {
|
|||
return
|
||||
}
|
||||
|
||||
cantSetUser := !ctx.Doer.IsAdmin &&
|
||||
cantSetUser := !ctx.IsUserSiteAdmin() &&
|
||||
opts.UserID != ctx.Doer.ID &&
|
||||
!ctx.IsUserRepoWriter([]unit.Type{unit.TypeIssues})
|
||||
|
||||
|
|
@ -203,7 +203,7 @@ func AddTime(ctx *context.APIContext) {
|
|||
|
||||
user := ctx.Doer
|
||||
if form.User != "" {
|
||||
if (ctx.IsUserRepoAdmin() && ctx.Doer.Name != form.User) || ctx.Doer.IsAdmin {
|
||||
if (ctx.IsUserRepoAdmin() && ctx.Doer.Name != form.User) || ctx.IsUserSiteAdmin() {
|
||||
// allow only RepoAdmin, Admin and User to add time
|
||||
user, err = user_model.GetUserByName(ctx, form.User)
|
||||
if err != nil {
|
||||
|
|
@ -371,7 +371,7 @@ func DeleteTime(ctx *context.APIContext) {
|
|||
return
|
||||
}
|
||||
|
||||
if !ctx.Doer.IsAdmin && time.UserID != ctx.Doer.ID {
|
||||
if !ctx.IsUserSiteAdmin() && time.UserID != ctx.Doer.ID {
|
||||
// Only Admin and User itself can delete their time
|
||||
ctx.Status(http.StatusForbidden)
|
||||
return
|
||||
|
|
@ -437,7 +437,7 @@ func ListTrackedTimesByUser(ctx *context.APIContext) {
|
|||
return
|
||||
}
|
||||
|
||||
if !ctx.IsUserRepoAdmin() && !ctx.Doer.IsAdmin && ctx.Doer.ID != user.ID {
|
||||
if !ctx.IsUserRepoAdmin() && !ctx.IsUserSiteAdmin() && ctx.Doer.ID != user.ID {
|
||||
ctx.Error(http.StatusForbidden, "", errors.New("query by user not allowed; not enough rights"))
|
||||
return
|
||||
}
|
||||
|
|
@ -538,7 +538,7 @@ func ListTrackedTimesByRepository(ctx *context.APIContext) {
|
|||
return
|
||||
}
|
||||
|
||||
cantSetUser := !ctx.Doer.IsAdmin &&
|
||||
cantSetUser := !ctx.IsUserSiteAdmin() &&
|
||||
opts.UserID != ctx.Doer.ID &&
|
||||
!ctx.IsUserRepoWriter([]unit.Type{unit.TypeIssues})
|
||||
|
||||
|
|
|
|||
|
|
@ -104,7 +104,7 @@ func ListDeployKeys(ctx *context.APIContext) {
|
|||
return
|
||||
}
|
||||
apiKeys[i] = convert.ToDeployKey(apiLink, keys[i])
|
||||
if ctx.Doer.IsAdmin || ((ctx.Repo.Repository.ID == keys[i].RepoID) && (ctx.Doer.ID == ctx.Repo.Owner.ID)) {
|
||||
if ctx.IsUserSiteAdmin() || ((ctx.Repo.Repository.ID == keys[i].RepoID) && (ctx.Doer.ID == ctx.Repo.Owner.ID)) {
|
||||
apiKeys[i], _ = appendPrivateInformation(ctx, apiKeys[i], keys[i], ctx.Repo.Repository)
|
||||
}
|
||||
}
|
||||
|
|
@ -166,7 +166,7 @@ func GetDeployKey(ctx *context.APIContext) {
|
|||
|
||||
apiLink := composeDeployKeysAPILink(ctx.Repo.Owner.Name, ctx.Repo.Repository.Name)
|
||||
apiKey := convert.ToDeployKey(apiLink, key)
|
||||
if ctx.Doer.IsAdmin || ((ctx.Repo.Repository.ID == key.RepoID) && (ctx.Doer.ID == ctx.Repo.Owner.ID)) {
|
||||
if ctx.IsUserSiteAdmin() || ((ctx.Repo.Repository.ID == key.RepoID) && (ctx.Doer.ID == ctx.Repo.Owner.ID)) {
|
||||
apiKey, _ = appendPrivateInformation(ctx, apiKey, key, ctx.Repo.Repository)
|
||||
}
|
||||
ctx.JSON(http.StatusOK, apiKey)
|
||||
|
|
|
|||
|
|
@ -91,7 +91,7 @@ func Migrate(ctx *context.APIContext) {
|
|||
return
|
||||
}
|
||||
|
||||
if !ctx.Doer.IsAdmin {
|
||||
if !ctx.IsUserSiteAdmin() {
|
||||
if !repoOwner.IsOrganization() && ctx.Doer.ID != repoOwner.ID {
|
||||
ctx.Error(http.StatusForbidden, "", "Given user is not an organization.")
|
||||
return
|
||||
|
|
|
|||
|
|
@ -402,7 +402,7 @@ func DeletePullReview(ctx *context.APIContext) {
|
|||
ctx.NotFound()
|
||||
return
|
||||
}
|
||||
if !ctx.Doer.IsAdmin && ctx.Doer.ID != review.ReviewerID {
|
||||
if !ctx.IsUserSiteAdmin() && ctx.Doer.ID != review.ReviewerID {
|
||||
ctx.Error(http.StatusForbidden, "only admin and user itself can delete a review", nil)
|
||||
return
|
||||
}
|
||||
|
|
@ -699,7 +699,7 @@ func prepareSingleReview(ctx *context.APIContext) (*issues_model.Review, *issues
|
|||
}
|
||||
|
||||
// make sure that the user has access to this review if it is pending
|
||||
if review.Type == issues_model.ReviewTypePending && review.ReviewerID != ctx.Doer.ID && !ctx.Doer.IsAdmin {
|
||||
if review.Type == issues_model.ReviewTypePending && review.ReviewerID != ctx.Doer.ID && !ctx.IsUserSiteAdmin() {
|
||||
ctx.NotFound("GetReviewByID")
|
||||
return nil, nil, true
|
||||
}
|
||||
|
|
@ -1080,7 +1080,7 @@ func DeletePullReviewComment(ctx *context.APIContext) {
|
|||
}
|
||||
|
||||
func dismissReview(ctx *context.APIContext, msg string, isDismiss, dismissPriors bool) {
|
||||
if !ctx.Repo.IsAdmin() {
|
||||
if !ctx.IsUserRepoAdmin() {
|
||||
ctx.Error(http.StatusForbidden, "", "Must be repo admin")
|
||||
return
|
||||
}
|
||||
|
|
|
|||
|
|
@ -419,12 +419,12 @@ func Generate(ctx *context.APIContext) {
|
|||
return
|
||||
}
|
||||
|
||||
if !ctx.Doer.IsAdmin && !ctxUser.IsOrganization() {
|
||||
if !ctx.IsUserSiteAdmin() && !ctxUser.IsOrganization() {
|
||||
ctx.Error(http.StatusForbidden, "", "Only admin can generate repository for other user.")
|
||||
return
|
||||
}
|
||||
|
||||
if !ctx.Doer.IsAdmin {
|
||||
if !ctx.IsUserSiteAdmin() {
|
||||
canCreate, err := organization.OrgFromUser(ctxUser).CanCreateOrgRepo(ctx, ctx.Doer.ID)
|
||||
if err != nil {
|
||||
ctx.ServerError("CanCreateOrgRepo", err)
|
||||
|
|
@ -534,7 +534,7 @@ func CreateOrgRepo(ctx *context.APIContext) {
|
|||
return
|
||||
}
|
||||
|
||||
if !ctx.Doer.IsAdmin {
|
||||
if !ctx.IsUserSiteAdmin() {
|
||||
canCreate, err := org.CanCreateOrgRepo(ctx, ctx.Doer.ID)
|
||||
if err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "CanCreateOrgRepo", err)
|
||||
|
|
@ -776,7 +776,7 @@ func updateBasicProperties(ctx *context.APIContext, opts api.EditRepoOption) err
|
|||
|
||||
visibilityChanged = repo.IsPrivate != *opts.Private
|
||||
// when ForcePrivate enabled, you could change public repo to private, but only admin users can change private to public
|
||||
if visibilityChanged && setting.Repository.ForcePrivate && !*opts.Private && !ctx.Doer.IsAdmin {
|
||||
if visibilityChanged && setting.Repository.ForcePrivate && !*opts.Private && !ctx.IsUserSiteAdmin() {
|
||||
err := errors.New("cannot change private repository to public")
|
||||
ctx.Error(http.StatusUnprocessableEntity, "Force Private enabled", err)
|
||||
return err
|
||||
|
|
|
|||
|
|
@ -72,7 +72,7 @@ func Transfer(ctx *context.APIContext) {
|
|||
}
|
||||
|
||||
if newOwner.Type == user_model.UserTypeOrganization {
|
||||
if !ctx.Doer.IsAdmin && newOwner.Visibility == api.VisibleTypePrivate && !organization.OrgFromUser(newOwner).HasMemberWithUserID(ctx, ctx.Doer.ID) {
|
||||
if !ctx.IsUserSiteAdmin() && newOwner.Visibility == api.VisibleTypePrivate && !organization.OrgFromUser(newOwner).HasMemberWithUserID(ctx, ctx.Doer.ID) {
|
||||
// The user shouldn't know about this organization
|
||||
ctx.Error(http.StatusNotFound, "", "The new owner does not exist or cannot be found")
|
||||
return
|
||||
|
|
|
|||
|
|
@ -70,7 +70,7 @@ func GetHook(ctx *context.APIContext) {
|
|||
return
|
||||
}
|
||||
|
||||
if !ctx.Doer.IsAdmin && hook.OwnerID != ctx.Doer.ID {
|
||||
if !ctx.IsUserSiteAdmin() && hook.OwnerID != ctx.Doer.ID {
|
||||
ctx.NotFound()
|
||||
return
|
||||
}
|
||||
|
|
|
|||
|
|
@ -90,7 +90,7 @@ func listPublicKeys(ctx *context.APIContext, user *user_model.User) {
|
|||
apiKeys := make([]*api.PublicKey, len(keys))
|
||||
for i := range keys {
|
||||
apiKeys[i] = convert.ToPublicKey(apiLink, keys[i])
|
||||
if ctx.Doer.IsAdmin || ctx.Doer.ID == keys[i].OwnerID {
|
||||
if ctx.IsUserSiteAdmin() || ctx.Doer.ID == keys[i].OwnerID {
|
||||
apiKeys[i], _ = appendPrivateInformation(ctx, apiKeys[i], keys[i], user)
|
||||
}
|
||||
}
|
||||
|
|
@ -200,7 +200,7 @@ func GetPublicKey(ctx *context.APIContext) {
|
|||
|
||||
apiLink := composePublicKeysAPILink()
|
||||
apiKey := convert.ToPublicKey(apiLink, key)
|
||||
if ctx.Doer.IsAdmin || ctx.Doer.ID == key.OwnerID {
|
||||
if ctx.IsUserSiteAdmin() || ctx.Doer.ID == key.OwnerID {
|
||||
apiKey, _ = appendPrivateInformation(ctx, apiKey, key, ctx.Doer)
|
||||
}
|
||||
ctx.JSON(http.StatusOK, apiKey)
|
||||
|
|
@ -226,7 +226,7 @@ func CreateUserPublicKey(ctx *context.APIContext, form api.CreateKeyOption, uid
|
|||
}
|
||||
apiLink := composePublicKeysAPILink()
|
||||
apiKey := convert.ToPublicKey(apiLink, key)
|
||||
if ctx.Doer.IsAdmin || ctx.Doer.ID == key.OwnerID {
|
||||
if ctx.IsUserSiteAdmin() || ctx.Doer.ID == key.OwnerID {
|
||||
apiKey, _ = appendPrivateInformation(ctx, apiKey, key, ctx.Doer)
|
||||
}
|
||||
ctx.JSON(http.StatusCreated, apiKey)
|
||||
|
|
|
|||
|
|
@ -43,7 +43,7 @@ func listUserRepos(ctx *context.APIContext, u *user_model.User, private bool) {
|
|||
ctx.Error(http.StatusInternalServerError, "GetUserRepoPermissionWithReducer", err)
|
||||
return
|
||||
}
|
||||
if ctx.IsSigned && ctx.Doer.IsAdmin || permission.HasAccess() {
|
||||
if ctx.IsSigned && ctx.IsUserSiteAdmin() || permission.HasAccess() {
|
||||
apiRepos = append(apiRepos, convert.ToRepo(ctx, repos[i], permission))
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -214,7 +214,7 @@ func ListUserActivityFeeds(ctx *context.APIContext) {
|
|||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
|
||||
includePrivate := ctx.IsSigned && (ctx.Doer.IsAdmin || ctx.Doer.ID == ctx.ContextUser.ID)
|
||||
includePrivate := ctx.IsSigned && (ctx.IsUserSiteAdmin() || ctx.Doer.ID == ctx.ContextUser.ID)
|
||||
listOptions := utils.GetListOptions(ctx)
|
||||
|
||||
opts := activities_model.GetFeedsOptions{
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue