Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 18 additions & 16 deletions modules/setting/oauth2.go
Original file line number Diff line number Diff line change
Expand Up @@ -90,23 +90,25 @@ func parseScopes(sec ConfigSection, name string) []string {
}

var OAuth2 = struct {
Enabled bool
AccessTokenExpirationTime int64
RefreshTokenExpirationTime int64
InvalidateRefreshTokens bool
JWTSigningAlgorithm string `ini:"JWT_SIGNING_ALGORITHM"`
JWTSigningPrivateKeyFile string `ini:"JWT_SIGNING_PRIVATE_KEY_FILE"`
MaxTokenLength int
DefaultApplications []string
Enabled bool
AccessTokenExpirationTime int64
RefreshTokenExpirationTime int64
InvalidateRefreshTokens bool
JWTSigningAlgorithm string `ini:"JWT_SIGNING_ALGORITHM"`
JWTSigningPrivateKeyFile string `ini:"JWT_SIGNING_PRIVATE_KEY_FILE"`
MaxTokenLength int
DefaultApplications []string
EnableAdditionalGrantScopes bool
}{
Enabled: true,
AccessTokenExpirationTime: 3600,
RefreshTokenExpirationTime: 730,
InvalidateRefreshTokens: false,
JWTSigningAlgorithm: "RS256",
JWTSigningPrivateKeyFile: "jwt/private.pem",
MaxTokenLength: math.MaxInt16,
DefaultApplications: []string{"git-credential-oauth", "git-credential-manager", "tea"},
Enabled: true,
AccessTokenExpirationTime: 3600,
RefreshTokenExpirationTime: 730,
InvalidateRefreshTokens: false,
JWTSigningAlgorithm: "RS256",
JWTSigningPrivateKeyFile: "jwt/private.pem",
MaxTokenLength: math.MaxInt16,
DefaultApplications: []string{"git-credential-oauth", "git-credential-manager", "tea"},
EnableAdditionalGrantScopes: false,
}

func loadOAuth2From(rootCfg ConfigProvider) {
Expand Down
4 changes: 2 additions & 2 deletions options/locale/locale_en-US.ini
Original file line number Diff line number Diff line change
Expand Up @@ -893,8 +893,8 @@ 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.
repo_and_org_access = Repository and Organization Access
permissions_public_only = Public only
permissions_access_all = All (public, private, and limited)
permissions_public_only = Public only (Grant access only to public and exclude private and limited organizations and repositories)
permissions_access_all = All (Grant access to public, private, and limited organizations and repositories)
select_permissions = Select permissions
permission_not_set = Not set
permission_no_access = No Access
Expand Down
31 changes: 19 additions & 12 deletions routers/api/v1/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ import (
"code.gitea.io/gitea/routers/api/v1/repo"
"code.gitea.io/gitea/routers/api/v1/settings"
"code.gitea.io/gitea/routers/api/v1/user"
"code.gitea.io/gitea/routers/api/v1/utils"
"code.gitea.io/gitea/routers/common"
"code.gitea.io/gitea/services/actions"
"code.gitea.io/gitea/services/auth"
Expand Down Expand Up @@ -184,6 +185,10 @@ func repoAssignment() func(ctx *context.APIContext) {
}
return
}
if repo.IsPrivate && utils.PublicOnlyToken(ctx, "ApiTokenScopePublicRepoOnly") {
ctx.NotFound()
return
}

repo.Owner = owner
ctx.Repo.Repository = repo
Expand Down Expand Up @@ -954,9 +959,9 @@ func Routes() *web.Router {
m.Get("/{target}", user.CheckFollowing)
})

m.Get("/starred", user.GetStarredRepos)
m.Get("/starred", tokenRequiresScopes(auth_model.AccessTokenScopeCategoryRepository), user.GetStarredRepos)

m.Get("/subscriptions", user.GetWatchedRepos)
m.Get("/subscriptions", tokenRequiresScopes(auth_model.AccessTokenScopeCategoryRepository), user.GetWatchedRepos)
}, context.UserAssignmentAPI())
}, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryUser), reqToken())

Expand Down Expand Up @@ -1476,13 +1481,13 @@ func Routes() *web.Router {
m.Get("/{org}/permissions", reqToken(), org.GetUserOrgsPermissions)
}, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryUser, auth_model.AccessTokenScopeCategoryOrganization), context.UserAssignmentAPI())
m.Post("/orgs", tokenRequiresScopes(auth_model.AccessTokenScopeCategoryOrganization), reqToken(), bind(api.CreateOrgOption{}), org.Create)
m.Get("/orgs", org.GetAll, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryOrganization))
m.Get("/orgs", tokenRequiresScopes(auth_model.AccessTokenScopeCategoryOrganization), org.GetAll)
m.Group("/orgs/{org}", func() {
m.Combo("").Get(org.Get).
Patch(reqToken(), reqOrgOwnership(), bind(api.EditOrgOption{}), org.Edit).
Delete(reqToken(), reqOrgOwnership(), org.Delete)
m.Combo("/repos").Get(user.ListOrgRepos).
Post(reqToken(), bind(api.CreateRepoOption{}), repo.CreateOrgRepo)
m.Combo("/repos").Get(tokenRequiresScopes(auth_model.AccessTokenScopeCategoryRepository), user.ListOrgRepos).
Post(reqToken(), tokenRequiresScopes(auth_model.AccessTokenScopeCategoryRepository), bind(api.CreateRepoOption{}), repo.CreateOrgRepo)
m.Group("/members", func() {
m.Get("", reqToken(), org.ListMembers)
m.Combo("/{username}").Get(reqToken(), org.IsMember).
Expand Down Expand Up @@ -1550,7 +1555,7 @@ func Routes() *web.Router {
Put(reqToken(), org.AddTeamRepository).
Delete(reqToken(), org.RemoveTeamRepository).
Get(reqToken(), org.GetTeamRepo)
})
}, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryRepository, auth_model.AccessTokenScopeCategoryRepository))
m.Get("/activities/feeds", org.ListTeamActivityFeeds)
}, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryOrganization), orgAssignment(false, true), reqToken(), reqTeamMembership())

Expand All @@ -1570,23 +1575,25 @@ func Routes() *web.Router {
m.Post("", bind(api.CreateKeyOption{}), admin.CreatePublicKey)
m.Delete("/{id}", admin.DeleteUserPublicKey)
})
m.Get("/orgs", org.ListUserOrgs)
m.Post("/orgs", bind(api.CreateOrgOption{}), admin.CreateOrg)
m.Post("/repos", bind(api.CreateRepoOption{}), admin.CreateRepo)
m.Get("/orgs", tokenRequiresScopes(auth_model.AccessTokenScopeCategoryOrganization), org.ListUserOrgs)
m.Get("/orgs", tokenRequiresScopes(auth_model.AccessTokenScopeCategoryOrganization), org.ListUserOrgs)
m.Post("/orgs", tokenRequiresScopes(auth_model.AccessTokenScopeCategoryOrganization), bind(api.CreateOrgOption{}), admin.CreateOrg)
m.Post("/repos", tokenRequiresScopes(auth_model.AccessTokenScopeCategoryRepository), bind(api.CreateRepoOption{}), admin.CreateRepo)
m.Post("/rename", bind(api.RenameUserOption{}), admin.RenameUser)
m.Get("/badges", admin.ListUserBadges)
m.Post("/badges", bind(api.UserBadgeOption{}), admin.AddUserBadges)
m.Delete("/badges", bind(api.UserBadgeOption{}), admin.DeleteUserBadges)
}, context.UserAssignmentAPI())
})
}, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryUser))

m.Group("/emails", func() {
m.Get("", admin.GetAllEmails)
m.Get("/search", admin.SearchEmail)
})
m.Group("/unadopted", func() {
m.Get("", admin.ListUnadoptedRepositories)
m.Post("/{username}/{reponame}", admin.AdoptRepository)
m.Delete("/{username}/{reponame}", admin.DeleteUnadoptedRepository)
m.Post("/{username}/{reponame}", tokenRequiresScopes(auth_model.AccessTokenScopeCategoryRepository), admin.AdoptRepository)
m.Delete("/{username}/{reponame}", tokenRequiresScopes(auth_model.AccessTokenScopeCategoryRepository), admin.DeleteUnadoptedRepository)
})
m.Group("/hooks", func() {
m.Combo("").Get(admin.ListHooks).
Expand Down
18 changes: 14 additions & 4 deletions routers/api/v1/org/org.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@ 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)
if utils.PublicOnlyToken(ctx, "ApiTokenScopePublicOrgOnly") {
showPrivate = false
}

opts := organization.FindOrgOptions{
ListOptions: listOptions,
Expand Down Expand Up @@ -191,10 +194,12 @@ func GetAll(ctx *context.APIContext) {
// "$ref": "#/responses/OrganizationList"

vMode := []api.VisibleType{api.VisibleTypePublic}
if ctx.IsSigned {
vMode = append(vMode, api.VisibleTypeLimited)
if ctx.Doer.IsAdmin {
vMode = append(vMode, api.VisibleTypePrivate)
if !utils.PublicOnlyToken(ctx, "ApiTokenScopePublicOrgOnly") {
if ctx.IsSigned {
vMode = append(vMode, api.VisibleTypeLimited)
if ctx.Doer.IsAdmin {
vMode = append(vMode, api.VisibleTypePrivate)
}
}
}

Expand Down Expand Up @@ -304,6 +309,11 @@ func Get(ctx *context.APIContext) {
return
}

if !ctx.Org.Organization.Visibility.IsPublic() && utils.PublicOnlyToken(ctx, "ApiTokenScopePublicOrgOnly") {
ctx.NotFound()
return
}

org := convert.ToOrganization(ctx, ctx.Org.Organization)

// Don't show Mail, when User is not logged in
Expand Down
11 changes: 11 additions & 0 deletions routers/api/v1/repo/repo.go
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,11 @@ func Search(ctx *context.APIContext) {
}
}

if utils.PublicOnlyToken(ctx, "ApiTokenScopePublicRepoOnly") {
opts.Private = false
opts.IsPrivate = optional.Some(false)
}

var err error
repos, count, err := repo_model.SearchRepository(ctx, opts)
if err != nil {
Expand Down Expand Up @@ -589,6 +594,12 @@ func GetByID(ctx *context.APIContext) {
ctx.NotFound()
return
}

if repo.IsPrivate && utils.PublicOnlyToken(ctx, "ApiTokenScopePublicRepoOnly") {
ctx.NotFound()
return
}

ctx.JSON(http.StatusOK, convert.ToRepo(ctx, repo, permission))
}

Expand Down
14 changes: 12 additions & 2 deletions routers/api/v1/user/repo.go
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,9 @@ func ListUserRepos(ctx *context.APIContext) {
// "$ref": "#/responses/notFound"

private := ctx.IsSigned
if utils.PublicOnlyToken(ctx, "ApiTokenScopePublicRepoOnly") {
private = false
}
listUserRepos(ctx, ctx.ContextUser, private)
}

Expand Down Expand Up @@ -111,6 +114,10 @@ func ListMyRepos(ctx *context.APIContext) {
IncludeDescription: true,
}

if utils.PublicOnlyToken(ctx, "ApiTokenScopePublicRepoOnly") {
opts.Private = false
}

var err error
repos, count, err := repo_model.SearchRepository(ctx, opts)
if err != nil {
Expand Down Expand Up @@ -162,6 +169,9 @@ func ListOrgRepos(ctx *context.APIContext) {
// "$ref": "#/responses/RepositoryList"
// "404":
// "$ref": "#/responses/notFound"

listUserRepos(ctx, ctx.Org.Organization.AsUser(), ctx.IsSigned)
private := ctx.IsSigned
if utils.PublicOnlyToken(ctx, "ApiTokenScopePublicRepoOnly") {
private = false
}
listUserRepos(ctx, ctx.Org.Organization.AsUser(), private)
}
12 changes: 12 additions & 0 deletions routers/api/v1/utils/token.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
// Copyright 2024 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT

package utils

import "code.gitea.io/gitea/services/context"

// check if api token contains `public-only` scope
func PublicOnlyToken(ctx *context.APIContext, scopeKey string) bool {
publicScope, _ := ctx.Data[scopeKey].(bool)
return publicScope
}
23 changes: 21 additions & 2 deletions routers/web/auth/oauth2_provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ type userInfoResponse struct {
Username string `json:"preferred_username"`
Email string `json:"email"`
Picture string `json:"picture"`
Groups []string `json:"groups"`
Groups []string `json:"groups,omitempty"`
}

// InfoOAuth manages request for userinfo endpoint
Expand All @@ -104,7 +104,19 @@ func InfoOAuth(ctx *context.Context) {
Picture: ctx.Doer.AvatarLink(ctx),
}

groups, err := oauth2_provider.GetOAuthGroupsForUser(ctx, ctx.Doer)
// groups, err := oauth2_provider.GetOAuthGroupsForUser(ctx, ctx.Doer)
var token string
if auHead := ctx.Req.Header.Get("Authorization"); auHead != "" {
auths := strings.Fields(auHead)
if len(auths) == 2 && (auths[0] == "token" || strings.ToLower(auths[0]) == "bearer") {
token = auths[1]
}
}

_, grantScopes := auth_service.CheckOAuthAccessToken(ctx, token)
onlyPublicGroups := oauth2_provider.IfOnlyPublicGroups(grantScopes)

groups, err := oauth2_provider.GetOAuthGroupsForUser(ctx, ctx.Doer, onlyPublicGroups)
if err != nil {
ctx.ServerError("Oauth groups for user", err)
return
Expand Down Expand Up @@ -304,6 +316,13 @@ func AuthorizeOAuth(ctx *context.Context) {
return
}

// check if additional scopes
if auth_service.GrantAdditionalScopes(form.Scope) == "" {
ctx.Data["AdditionalScopes"] = false
} else {
ctx.Data["AdditionalScopes"] = true
}

// show authorize page to grant access
ctx.Data["Application"] = app
ctx.Data["RedirectURI"] = form.RedirectURI
Expand Down
1 change: 1 addition & 0 deletions routers/web/user/setting/applications.go
Original file line number Diff line number Diff line change
Expand Up @@ -113,5 +113,6 @@ func loadApplicationsData(ctx *context.Context) {
ctx.ServerError("GetOAuth2GrantsByUserID", err)
return
}
ctx.Data["EnableAdditionalGrantScopes"] = setting.OAuth2.EnableAdditionalGrantScopes
}
}
35 changes: 35 additions & 0 deletions services/auth/additional_scopes_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
// Copyright 2024 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT

package auth

import (
"testing"

"github.com/stretchr/testify/assert"
)

func TestGrantAdditionalScopes(t *testing.T) {
tests := []struct {
grantScopes string
expectedScopes string
}{
{"openid profile email", ""},
{"openid profile email groups", ""},
{"openid profile email all", "all"},
{"openid profile email read:user all", "read:user,all"},
{"openid profile email groups read:user", "read:user"},
{"read:user read:repository", "read:user,read:repository"},
{"read:user write:issue public-only", "read:user,write:issue,public-only"},
{"openid profile email read:user", "read:user"},
{"read:invalid_scope", ""},
{"read:invalid_scope,write:scope_invalid,just-plain-wrong", ""},
}

for _, test := range tests {
t.Run(test.grantScopes, func(t *testing.T) {
result := GrantAdditionalScopes(test.grantScopes)
assert.Equal(t, test.expectedScopes, result)
})
}
}
2 changes: 1 addition & 1 deletion services/auth/basic.go
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ func (b *Basic) Verify(req *http.Request, w http.ResponseWriter, store DataStore
}

// check oauth2 token
uid := CheckOAuthAccessToken(req.Context(), authToken)
uid, _ := CheckOAuthAccessToken(req.Context(), authToken)
if uid != 0 {
log.Trace("Basic Authorization: Valid OAuthAccessToken for user[%d]", uid)

Expand Down
Loading