Merge branch 'main' of github.com:go-gitea/gitea into not7cd/commit-status-token-scope

This commit is contained in:
Norbert Szulc committed 2025-08-29 19:59:09 +00:00
commit 7410534629
3628 files changed
+127896 -133729

No files matched your search

+18 -7
View File
@@ -5,6 +5,7 @@ package auth
import (
"fmt"
"slices"
"strings"
"code.gitea.io/gitea/models/perm"
@@ -14,7 +15,7 @@ import (
type AccessTokenScopeCategory int
const (
AccessTokenScopeCategoryActivityPub = iota
AccessTokenScopeCategoryActivityPub AccessTokenScopeCategory = iota
AccessTokenScopeCategoryAdmin
AccessTokenScopeCategoryMisc // WARN: this is now just a placeholder, don't remove it which will change the following values
AccessTokenScopeCategoryNotification
@@ -206,6 +207,14 @@ var accessTokenScopes = map[AccessTokenScopeLevel]map[AccessTokenScopeCategory]A
},
}
func GetAccessTokenCategories() (res []string) {
for _, cat := range accessTokenScopes[Read] {
res = append(res, strings.TrimPrefix(string(cat), "read:"))
}
slices.Sort(res)
return res
}
// GetRequiredScopes gets the specific scopes for a given level and categories
func GetRequiredScopes(level AccessTokenScopeLevel, scopeCategories ...AccessTokenScopeCategory) []AccessTokenScope {
scopes := make([]AccessTokenScope, 0, len(scopeCategories))
@@ -217,12 +226,7 @@ func GetRequiredScopes(level AccessTokenScopeLevel, scopeCategories ...AccessTok
// 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
return slices.Contains(categories, category)
}
// GetScopeLevelFromAccessMode converts permission access mode to scope level
@@ -283,6 +287,9 @@ func (s AccessTokenScope) parse() (accessTokenScopeBitmap, error) {
// StringSlice returns the AccessTokenScope as a []string
func (s AccessTokenScope) StringSlice() []string {
if s == "" {
return nil
}
return strings.Split(string(s), ",")
}
@@ -296,6 +303,10 @@ func (s AccessTokenScope) Normalize() (AccessTokenScope, error) {
return bitmap.toScope(), nil
}
func (s AccessTokenScope) HasPermissionScope() bool {
return s != "" && s != AccessTokenScopePublicOnly
}
// PublicOnly checks if this token scope is limited to public resources
func (s AccessTokenScope) PublicOnly() (bool, error) {
bitmap, err := s.parse()
+16 -15
View File
@@ -17,6 +17,7 @@ type scopeTestNormalize struct {
}
func TestAccessTokenScope_Normalize(t *testing.T) {
assert.Equal(t, []string{"activitypub", "admin", "issue", "misc", "notification", "organization", "package", "repository", "user", "commitstatus"}, GetAccessTokenCategories())
tests := []scopeTestNormalize{
{"", "", nil},
{"write:misc,write:notification,read:package,write:notification,public-only", "public-only,write:misc,write:notification,read:package", nil},
@@ -25,13 +26,13 @@ func TestAccessTokenScope_Normalize(t *testing.T) {
{"write:activitypub,write:admin,write:misc,write:notification,write:organization,write:package,write:issue,write:repository,write:user,write:commitstatus,public-only", "public-only,all", nil},
}
for _, scope := range []string{"activitypub", "admin", "misc", "notification", "organization", "package", "issue", "repository", "user", "commitstatus"} {
for _, scope := range GetAccessTokenCategories() {
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},
scopeTestNormalize{AccessTokenScope("read:" + scope), AccessTokenScope("read:" + scope), nil},
scopeTestNormalize{AccessTokenScope("write:" + scope), AccessTokenScope("write:" + scope), nil},
scopeTestNormalize{AccessTokenScope(fmt.Sprintf("write:%[1]s,read:%[1]s", scope)), AccessTokenScope("write:" + scope), nil},
scopeTestNormalize{AccessTokenScope(fmt.Sprintf("read:%[1]s,write:%[1]s", scope)), AccessTokenScope("write:" + scope), nil},
scopeTestNormalize{AccessTokenScope(fmt.Sprintf("read:%[1]s,write:%[1]s,write:%[1]s", scope)), AccessTokenScope("write:" + scope), nil},
)
}
@@ -59,23 +60,23 @@ func TestAccessTokenScope_HasScope(t *testing.T) {
{"public-only", "read:issue", false, nil},
}
for _, scope := range []string{"activitypub", "admin", "misc", "notification", "organization", "package", "issue", "repository", "user"} {
for _, scope := range GetAccessTokenCategories() {
tests = append(tests,
scopeTestHasScope{
AccessTokenScope(fmt.Sprintf("read:%s", scope)),
AccessTokenScope(fmt.Sprintf("read:%s", scope)), true, nil,
AccessTokenScope("read:" + scope),
AccessTokenScope("read:" + scope), true, nil,
},
scopeTestHasScope{
AccessTokenScope(fmt.Sprintf("write:%s", scope)),
AccessTokenScope(fmt.Sprintf("write:%s", scope)), true, nil,
AccessTokenScope("write:" + scope),
AccessTokenScope("write:" + scope), true, nil,
},
scopeTestHasScope{
AccessTokenScope(fmt.Sprintf("write:%s", scope)),
AccessTokenScope(fmt.Sprintf("read:%s", scope)), true, nil,
AccessTokenScope("write:" + scope),
AccessTokenScope("read:" + scope), true, nil,
},
scopeTestHasScope{
AccessTokenScope(fmt.Sprintf("read:%s", scope)),
AccessTokenScope(fmt.Sprintf("write:%s", scope)), false, nil,
AccessTokenScope("read:" + scope),
AccessTokenScope("write:" + scope), false, nil,
},
)
}
+17 -17
View File
@@ -19,7 +19,7 @@ func TestNewAccessToken(t *testing.T) {
UID: 3,
Name: "Token C",
}
assert.NoError(t, auth_model.NewAccessToken(db.DefaultContext, token))
assert.NoError(t, auth_model.NewAccessToken(t.Context(), token))
unittest.AssertExistsAndLoadBean(t, token)
invalidToken := &auth_model.AccessToken{
@@ -27,7 +27,7 @@ func TestNewAccessToken(t *testing.T) {
UID: 2,
Name: "Token F",
}
assert.Error(t, auth_model.NewAccessToken(db.DefaultContext, invalidToken))
assert.Error(t, auth_model.NewAccessToken(t.Context(), invalidToken))
}
func TestAccessTokenByNameExists(t *testing.T) {
@@ -40,16 +40,16 @@ func TestAccessTokenByNameExists(t *testing.T) {
}
// Check to make sure it doesn't exists already
exist, err := auth_model.AccessTokenByNameExists(db.DefaultContext, token)
exist, err := auth_model.AccessTokenByNameExists(t.Context(), token)
assert.NoError(t, err)
assert.False(t, exist)
// Save it to the database
assert.NoError(t, auth_model.NewAccessToken(db.DefaultContext, token))
assert.NoError(t, auth_model.NewAccessToken(t.Context(), token))
unittest.AssertExistsAndLoadBean(t, token)
// This token must be found by name in the DB now
exist, err = auth_model.AccessTokenByNameExists(db.DefaultContext, token)
exist, err = auth_model.AccessTokenByNameExists(t.Context(), token)
assert.NoError(t, err)
assert.True(t, exist)
@@ -60,32 +60,32 @@ func TestAccessTokenByNameExists(t *testing.T) {
// Name matches but different user ID, this shouldn't exists in the
// database
exist, err = auth_model.AccessTokenByNameExists(db.DefaultContext, user4Token)
exist, err = auth_model.AccessTokenByNameExists(t.Context(), user4Token)
assert.NoError(t, err)
assert.False(t, exist)
}
func TestGetAccessTokenBySHA(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
token, err := auth_model.GetAccessTokenBySHA(db.DefaultContext, "d2c6c1ba3890b309189a8e618c72a162e4efbf36")
token, err := auth_model.GetAccessTokenBySHA(t.Context(), "d2c6c1ba3890b309189a8e618c72a162e4efbf36")
assert.NoError(t, err)
assert.Equal(t, int64(1), token.UID)
assert.Equal(t, "Token A", token.Name)
assert.Equal(t, "2b3668e11cb82d3af8c6e4524fc7841297668f5008d1626f0ad3417e9fa39af84c268248b78c481daa7e5dc437784003494f", token.TokenHash)
assert.Equal(t, "e4efbf36", token.TokenLastEight)
_, err = auth_model.GetAccessTokenBySHA(db.DefaultContext, "notahash")
_, err = auth_model.GetAccessTokenBySHA(t.Context(), "notahash")
assert.Error(t, err)
assert.True(t, auth_model.IsErrAccessTokenNotExist(err))
_, err = auth_model.GetAccessTokenBySHA(db.DefaultContext, "")
_, err = auth_model.GetAccessTokenBySHA(t.Context(), "")
assert.Error(t, err)
assert.True(t, auth_model.IsErrAccessTokenEmpty(err))
}
func TestListAccessTokens(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
tokens, err := db.Find[auth_model.AccessToken](db.DefaultContext, auth_model.ListAccessTokensOptions{UserID: 1})
tokens, err := db.Find[auth_model.AccessToken](t.Context(), auth_model.ListAccessTokensOptions{UserID: 1})
assert.NoError(t, err)
if assert.Len(t, tokens, 2) {
assert.Equal(t, int64(1), tokens[0].UID)
@@ -94,39 +94,39 @@ func TestListAccessTokens(t *testing.T) {
assert.Contains(t, []string{tokens[0].Name, tokens[1].Name}, "Token B")
}
tokens, err = db.Find[auth_model.AccessToken](db.DefaultContext, auth_model.ListAccessTokensOptions{UserID: 2})
tokens, err = db.Find[auth_model.AccessToken](t.Context(), auth_model.ListAccessTokensOptions{UserID: 2})
assert.NoError(t, err)
if assert.Len(t, tokens, 1) {
assert.Equal(t, int64(2), tokens[0].UID)
assert.Equal(t, "Token A", tokens[0].Name)
}
tokens, err = db.Find[auth_model.AccessToken](db.DefaultContext, auth_model.ListAccessTokensOptions{UserID: 100})
tokens, err = db.Find[auth_model.AccessToken](t.Context(), auth_model.ListAccessTokensOptions{UserID: 100})
assert.NoError(t, err)
assert.Empty(t, tokens)
}
func TestUpdateAccessToken(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
token, err := auth_model.GetAccessTokenBySHA(db.DefaultContext, "4c6f36e6cf498e2a448662f915d932c09c5a146c")
token, err := auth_model.GetAccessTokenBySHA(t.Context(), "4c6f36e6cf498e2a448662f915d932c09c5a146c")
assert.NoError(t, err)
token.Name = "Token Z"
assert.NoError(t, auth_model.UpdateAccessToken(db.DefaultContext, token))
assert.NoError(t, auth_model.UpdateAccessToken(t.Context(), token))
unittest.AssertExistsAndLoadBean(t, token)
}
func TestDeleteAccessTokenByID(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
token, err := auth_model.GetAccessTokenBySHA(db.DefaultContext, "4c6f36e6cf498e2a448662f915d932c09c5a146c")
token, err := auth_model.GetAccessTokenBySHA(t.Context(), "4c6f36e6cf498e2a448662f915d932c09c5a146c")
assert.NoError(t, err)
assert.Equal(t, int64(1), token.UID)
assert.NoError(t, auth_model.DeleteAccessTokenByID(db.DefaultContext, token.ID, 1))
assert.NoError(t, auth_model.DeleteAccessTokenByID(t.Context(), token.ID, 1))
unittest.AssertNotExistsBean(t, token)
err = auth_model.DeleteAccessTokenByID(db.DefaultContext, 100, 100)
err = auth_model.DeleteAccessTokenByID(t.Context(), 100, 100)
assert.Error(t, err)
assert.True(t, auth_model.IsErrAccessTokenNotExist(err))
}
+1 -1
View File
@@ -15,7 +15,7 @@ import (
var ErrAuthTokenNotExist = util.NewNotExistErrorf("auth token does not exist")
type AuthToken struct { //nolint:revive
type AuthToken struct { //nolint:revive // export stutter
ID string `xorm:"pk"`
TokenHash string
UserID int64 `xorm:"INDEX"`
+37 -51
View File
@@ -12,6 +12,7 @@ import (
"fmt"
"net"
"net/url"
"slices"
"strings"
"code.gitea.io/gitea/models/db"
@@ -288,35 +289,31 @@ type UpdateOAuth2ApplicationOptions struct {
// UpdateOAuth2Application updates an oauth2 application
func UpdateOAuth2Application(ctx context.Context, opts UpdateOAuth2ApplicationOptions) (*OAuth2Application, error) {
ctx, committer, err := db.TxContext(ctx)
if err != nil {
return nil, err
}
defer committer.Close()
return db.WithTx2(ctx, func(ctx context.Context) (*OAuth2Application, error) {
app, err := GetOAuth2ApplicationByID(ctx, opts.ID)
if err != nil {
return nil, err
}
if app.UID != opts.UserID {
return nil, errors.New("UID mismatch")
}
builtinApps := BuiltinApplications()
if _, builtin := builtinApps[app.ClientID]; builtin {
return nil, fmt.Errorf("failed to edit OAuth2 application: application is locked: %s", app.ClientID)
}
app, err := GetOAuth2ApplicationByID(ctx, opts.ID)
if err != nil {
return nil, err
}
if app.UID != opts.UserID {
return nil, errors.New("UID mismatch")
}
builtinApps := BuiltinApplications()
if _, builtin := builtinApps[app.ClientID]; builtin {
return nil, fmt.Errorf("failed to edit OAuth2 application: application is locked: %s", app.ClientID)
}
app.Name = opts.Name
app.RedirectURIs = opts.RedirectURIs
app.ConfidentialClient = opts.ConfidentialClient
app.SkipSecondaryAuthorization = opts.SkipSecondaryAuthorization
app.Name = opts.Name
app.RedirectURIs = opts.RedirectURIs
app.ConfidentialClient = opts.ConfidentialClient
app.SkipSecondaryAuthorization = opts.SkipSecondaryAuthorization
if err = updateOAuth2Application(ctx, app); err != nil {
return nil, err
}
app.ClientSecret = ""
if err = updateOAuth2Application(ctx, app); err != nil {
return nil, err
}
app.ClientSecret = ""
return app, committer.Commit()
return app, nil
})
}
func updateOAuth2Application(ctx context.Context, app *OAuth2Application) error {
@@ -357,23 +354,17 @@ func deleteOAuth2Application(ctx context.Context, id, userid int64) error {
// DeleteOAuth2Application deletes the application with the given id and the grants and auth codes related to it. It checks if the userid was the creator of the app.
func DeleteOAuth2Application(ctx context.Context, id, userid int64) error {
ctx, committer, err := db.TxContext(ctx)
if err != nil {
return err
}
defer committer.Close()
app, err := GetOAuth2ApplicationByID(ctx, id)
if err != nil {
return err
}
builtinApps := BuiltinApplications()
if _, builtin := builtinApps[app.ClientID]; builtin {
return fmt.Errorf("failed to delete OAuth2 application: application is locked: %s", app.ClientID)
}
if err := deleteOAuth2Application(ctx, id, userid); err != nil {
return err
}
return committer.Commit()
return db.WithTx(ctx, func(ctx context.Context) error {
app, err := GetOAuth2ApplicationByID(ctx, id)
if err != nil {
return err
}
builtinApps := BuiltinApplications()
if _, builtin := builtinApps[app.ClientID]; builtin {
return fmt.Errorf("failed to delete OAuth2 application: application is locked: %s", app.ClientID)
}
return deleteOAuth2Application(ctx, id, userid)
})
}
//////////////////////////////////////////////////////
@@ -511,12 +502,7 @@ func (grant *OAuth2Grant) IncreaseCounter(ctx context.Context) error {
// ScopeContains returns true if the grant scope contains the specified scope
func (grant *OAuth2Grant) ScopeContains(scope string) bool {
for _, currentScope := range strings.Split(grant.Scope, " ") {
if scope == currentScope {
return true
}
}
return false
return slices.Contains(strings.Split(grant.Scope, " "), scope)
}
// SetNonce updates the current nonce value of a grant
@@ -616,8 +602,8 @@ func (err ErrOAuthApplicationNotFound) Unwrap() error {
return util.ErrNotExist
}
// GetActiveOAuth2SourceByName returns a OAuth2 AuthSource based on the given name
func GetActiveOAuth2SourceByName(ctx context.Context, name string) (*Source, error) {
// GetActiveOAuth2SourceByAuthName returns a OAuth2 AuthSource based on the given name
func GetActiveOAuth2SourceByAuthName(ctx context.Context, name string) (*Source, error) {
authSource := new(Source)
has, err := db.GetEngine(ctx).Where("name = ? and type = ? and is_active = ?", name, OAuth2, true).Get(authSource)
if err != nil {
+23 -24
View File
@@ -7,7 +7,6 @@ import (
"testing"
auth_model "code.gitea.io/gitea/models/auth"
"code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/models/unittest"
"github.com/stretchr/testify/assert"
@@ -16,17 +15,17 @@ import (
func TestOAuth2Application_GenerateClientSecret(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
app := unittest.AssertExistsAndLoadBean(t, &auth_model.OAuth2Application{ID: 1})
secret, err := app.GenerateClientSecret(db.DefaultContext)
secret, err := app.GenerateClientSecret(t.Context())
assert.NoError(t, err)
assert.True(t, len(secret) > 0)
assert.NotEmpty(t, secret)
unittest.AssertExistsAndLoadBean(t, &auth_model.OAuth2Application{ID: 1, ClientSecret: app.ClientSecret})
}
func BenchmarkOAuth2Application_GenerateClientSecret(b *testing.B) {
assert.NoError(b, unittest.PrepareTestDatabase())
app := unittest.AssertExistsAndLoadBean(b, &auth_model.OAuth2Application{ID: 1})
for i := 0; i < b.N; i++ {
_, _ = app.GenerateClientSecret(db.DefaultContext)
for b.Loop() {
_, _ = app.GenerateClientSecret(b.Context())
}
}
@@ -76,7 +75,7 @@ func TestOAuth2Application_ContainsRedirect_Slash(t *testing.T) {
func TestOAuth2Application_ValidateClientSecret(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
app := unittest.AssertExistsAndLoadBean(t, &auth_model.OAuth2Application{ID: 1})
secret, err := app.GenerateClientSecret(db.DefaultContext)
secret, err := app.GenerateClientSecret(t.Context())
assert.NoError(t, err)
assert.True(t, app.ValidateClientSecret([]byte(secret)))
assert.False(t, app.ValidateClientSecret([]byte("fewijfowejgfiowjeoifew")))
@@ -84,18 +83,18 @@ func TestOAuth2Application_ValidateClientSecret(t *testing.T) {
func TestGetOAuth2ApplicationByClientID(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
app, err := auth_model.GetOAuth2ApplicationByClientID(db.DefaultContext, "da7da3ba-9a13-4167-856f-3899de0b0138")
app, err := auth_model.GetOAuth2ApplicationByClientID(t.Context(), "da7da3ba-9a13-4167-856f-3899de0b0138")
assert.NoError(t, err)
assert.Equal(t, "da7da3ba-9a13-4167-856f-3899de0b0138", app.ClientID)
app, err = auth_model.GetOAuth2ApplicationByClientID(db.DefaultContext, "invalid client id")
app, err = auth_model.GetOAuth2ApplicationByClientID(t.Context(), "invalid client id")
assert.Error(t, err)
assert.Nil(t, app)
}
func TestCreateOAuth2Application(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
app, err := auth_model.CreateOAuth2Application(db.DefaultContext, auth_model.CreateOAuth2ApplicationOptions{Name: "newapp", UserID: 1})
app, err := auth_model.CreateOAuth2Application(t.Context(), auth_model.CreateOAuth2ApplicationOptions{Name: "newapp", UserID: 1})
assert.NoError(t, err)
assert.Equal(t, "newapp", app.Name)
assert.Len(t, app.ClientID, 36)
@@ -109,11 +108,11 @@ func TestOAuth2Application_TableName(t *testing.T) {
func TestOAuth2Application_GetGrantByUserID(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
app := unittest.AssertExistsAndLoadBean(t, &auth_model.OAuth2Application{ID: 1})
grant, err := app.GetGrantByUserID(db.DefaultContext, 1)
grant, err := app.GetGrantByUserID(t.Context(), 1)
assert.NoError(t, err)
assert.Equal(t, int64(1), grant.UserID)
grant, err = app.GetGrantByUserID(db.DefaultContext, 34923458)
grant, err = app.GetGrantByUserID(t.Context(), 34923458)
assert.NoError(t, err)
assert.Nil(t, grant)
}
@@ -121,23 +120,23 @@ func TestOAuth2Application_GetGrantByUserID(t *testing.T) {
func TestOAuth2Application_CreateGrant(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
app := unittest.AssertExistsAndLoadBean(t, &auth_model.OAuth2Application{ID: 1})
grant, err := app.CreateGrant(db.DefaultContext, 2, "")
grant, err := app.CreateGrant(t.Context(), 2, "")
assert.NoError(t, err)
assert.NotNil(t, grant)
assert.Equal(t, int64(2), grant.UserID)
assert.Equal(t, int64(1), grant.ApplicationID)
assert.Equal(t, "", grant.Scope)
assert.Empty(t, grant.Scope)
}
//////////////////// Grant
func TestGetOAuth2GrantByID(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
grant, err := auth_model.GetOAuth2GrantByID(db.DefaultContext, 1)
grant, err := auth_model.GetOAuth2GrantByID(t.Context(), 1)
assert.NoError(t, err)
assert.Equal(t, int64(1), grant.ID)
grant, err = auth_model.GetOAuth2GrantByID(db.DefaultContext, 34923458)
grant, err = auth_model.GetOAuth2GrantByID(t.Context(), 34923458)
assert.NoError(t, err)
assert.Nil(t, grant)
}
@@ -145,7 +144,7 @@ func TestGetOAuth2GrantByID(t *testing.T) {
func TestOAuth2Grant_IncreaseCounter(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
grant := unittest.AssertExistsAndLoadBean(t, &auth_model.OAuth2Grant{ID: 1, Counter: 1})
assert.NoError(t, grant.IncreaseCounter(db.DefaultContext))
assert.NoError(t, grant.IncreaseCounter(t.Context()))
assert.Equal(t, int64(2), grant.Counter)
unittest.AssertExistsAndLoadBean(t, &auth_model.OAuth2Grant{ID: 1, Counter: 2})
}
@@ -162,10 +161,10 @@ func TestOAuth2Grant_ScopeContains(t *testing.T) {
func TestOAuth2Grant_GenerateNewAuthorizationCode(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
grant := unittest.AssertExistsAndLoadBean(t, &auth_model.OAuth2Grant{ID: 1})
code, err := grant.GenerateNewAuthorizationCode(db.DefaultContext, "https://example2.com/callback", "CjvyTLSdR47G5zYenDA-eDWW4lRrO8yvjcWwbD_deOg", "S256")
code, err := grant.GenerateNewAuthorizationCode(t.Context(), "https://example2.com/callback", "CjvyTLSdR47G5zYenDA-eDWW4lRrO8yvjcWwbD_deOg", "S256")
assert.NoError(t, err)
assert.NotNil(t, code)
assert.True(t, len(code.Code) > 32) // secret length > 32
assert.Greater(t, len(code.Code), 32) // secret length > 32
}
func TestOAuth2Grant_TableName(t *testing.T) {
@@ -174,20 +173,20 @@ func TestOAuth2Grant_TableName(t *testing.T) {
func TestGetOAuth2GrantsByUserID(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
result, err := auth_model.GetOAuth2GrantsByUserID(db.DefaultContext, 1)
result, err := auth_model.GetOAuth2GrantsByUserID(t.Context(), 1)
assert.NoError(t, err)
assert.Len(t, result, 1)
assert.Equal(t, int64(1), result[0].ID)
assert.Equal(t, result[0].ApplicationID, result[0].Application.ID)
result, err = auth_model.GetOAuth2GrantsByUserID(db.DefaultContext, 34134)
result, err = auth_model.GetOAuth2GrantsByUserID(t.Context(), 34134)
assert.NoError(t, err)
assert.Empty(t, result)
}
func TestRevokeOAuth2Grant(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
assert.NoError(t, auth_model.RevokeOAuth2Grant(db.DefaultContext, 1, 1))
assert.NoError(t, auth_model.RevokeOAuth2Grant(t.Context(), 1, 1))
unittest.AssertNotExistsBean(t, &auth_model.OAuth2Grant{ID: 1, UserID: 1})
}
@@ -195,13 +194,13 @@ func TestRevokeOAuth2Grant(t *testing.T) {
func TestGetOAuth2AuthorizationByCode(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
code, err := auth_model.GetOAuth2AuthorizationByCode(db.DefaultContext, "authcode")
code, err := auth_model.GetOAuth2AuthorizationByCode(t.Context(), "authcode")
assert.NoError(t, err)
assert.NotNil(t, code)
assert.Equal(t, "authcode", code.Code)
assert.Equal(t, int64(1), code.ID)
code, err = auth_model.GetOAuth2AuthorizationByCode(db.DefaultContext, "does not exist")
code, err = auth_model.GetOAuth2AuthorizationByCode(t.Context(), "does not exist")
assert.NoError(t, err)
assert.Nil(t, code)
}
@@ -256,7 +255,7 @@ func TestOAuth2AuthorizationCode_GenerateRedirectURI(t *testing.T) {
func TestOAuth2AuthorizationCode_Invalidate(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
code := unittest.AssertExistsAndLoadBean(t, &auth_model.OAuth2AuthorizationCode{Code: "authcode"})
assert.NoError(t, code.Invalidate(db.DefaultContext))
assert.NoError(t, code.Invalidate(t.Context()))
unittest.AssertNotExistsBean(t, &auth_model.OAuth2AuthorizationCode{Code: "authcode"})
}
+37 -45
View File
@@ -35,26 +35,22 @@ func UpdateSession(ctx context.Context, key string, data []byte) error {
// ReadSession reads the data for the provided session
func ReadSession(ctx context.Context, key string) (*Session, error) {
ctx, committer, err := db.TxContext(ctx)
if err != nil {
return nil, err
}
defer committer.Close()
session, exist, err := db.Get[Session](ctx, builder.Eq{"`key`": key})
if err != nil {
return nil, err
} else if !exist {
session = &Session{
Key: key,
Expiry: timeutil.TimeStampNow(),
}
if err := db.Insert(ctx, session); err != nil {
return db.WithTx2(ctx, func(ctx context.Context) (*Session, error) {
session, exist, err := db.Get[Session](ctx, builder.Eq{"`key`": key})
if err != nil {
return nil, err
} else if !exist {
session = &Session{
Key: key,
Expiry: timeutil.TimeStampNow(),
}
if err := db.Insert(ctx, session); err != nil {
return nil, err
}
}
}
return session, committer.Commit()
return session, nil
})
}
// ExistSession checks if a session exists
@@ -72,40 +68,36 @@ func DestroySession(ctx context.Context, key string) error {
// RegenerateSession regenerates a session from the old id
func RegenerateSession(ctx context.Context, oldKey, newKey string) (*Session, error) {
ctx, committer, err := db.TxContext(ctx)
if err != nil {
return nil, err
}
defer committer.Close()
return db.WithTx2(ctx, func(ctx context.Context) (*Session, error) {
if has, err := db.Exist[Session](ctx, builder.Eq{"`key`": newKey}); err != nil {
return nil, err
} else if has {
return nil, fmt.Errorf("session Key: %s already exists", newKey)
}
if has, err := db.Exist[Session](ctx, builder.Eq{"`key`": newKey}); err != nil {
return nil, err
} else if has {
return nil, fmt.Errorf("session Key: %s already exists", newKey)
}
if has, err := db.Exist[Session](ctx, builder.Eq{"`key`": oldKey}); err != nil {
return nil, err
} else if !has {
if err := db.Insert(ctx, &Session{
Key: oldKey,
Expiry: timeutil.TimeStampNow(),
}); err != nil {
return nil, err
}
}
if has, err := db.Exist[Session](ctx, builder.Eq{"`key`": oldKey}); err != nil {
return nil, err
} else if !has {
if err := db.Insert(ctx, &Session{
Key: oldKey,
Expiry: timeutil.TimeStampNow(),
}); err != nil {
if _, err := db.Exec(ctx, "UPDATE `session` SET `key` = ? WHERE `key`=?", newKey, oldKey); err != nil {
return nil, err
}
}
if _, err := db.Exec(ctx, "UPDATE "+db.TableName(&Session{})+" SET `key` = ? WHERE `key`=?", newKey, oldKey); err != nil {
return nil, err
}
s, _, err := db.Get[Session](ctx, builder.Eq{"`key`": newKey})
if err != nil {
// is not exist, it should be impossible
return nil, err
}
s, _, err := db.Get[Session](ctx, builder.Eq{"`key`": newKey})
if err != nil {
// is not exist, it should be impossible
return nil, err
}
return s, committer.Commit()
return s, nil
})
}
// CountSessions returns the number of sessions
+24 -21
View File
@@ -58,6 +58,15 @@ var Names = map[Type]string{
// Config represents login config as far as the db is concerned
type Config interface {
convert.Conversion
SetAuthSource(*Source)
}
type ConfigBase struct {
AuthSource *Source
}
func (p *ConfigBase) SetAuthSource(s *Source) {
p.AuthSource = s
}
// SkipVerifiable configurations provide a IsSkipVerify to check if SkipVerify is set
@@ -104,19 +113,15 @@ func RegisterTypeConfig(typ Type, exemplar Config) {
}
}
// SourceSettable configurations can have their authSource set on them
type SourceSettable interface {
SetAuthSource(*Source)
}
// Source represents an external way for authorizing users.
type Source struct {
ID int64 `xorm:"pk autoincr"`
Type Type
Name string `xorm:"UNIQUE"`
IsActive bool `xorm:"INDEX NOT NULL DEFAULT false"`
IsSyncEnabled bool `xorm:"INDEX NOT NULL DEFAULT false"`
Cfg convert.Conversion `xorm:"TEXT"`
ID int64 `xorm:"pk autoincr"`
Type Type
Name string `xorm:"UNIQUE"`
IsActive bool `xorm:"INDEX NOT NULL DEFAULT false"`
IsSyncEnabled bool `xorm:"INDEX NOT NULL DEFAULT false"`
TwoFactorPolicy string `xorm:"two_factor_policy NOT NULL DEFAULT ''"`
Cfg Config `xorm:"TEXT"`
CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"`
UpdatedUnix timeutil.TimeStamp `xorm:"INDEX updated"`
@@ -140,9 +145,7 @@ func (source *Source) BeforeSet(colName string, val xorm.Cell) {
return
}
source.Cfg = constructor()
if settable, ok := source.Cfg.(SourceSettable); ok {
settable.SetAuthSource(source)
}
source.Cfg.SetAuthSource(source)
}
}
@@ -200,6 +203,10 @@ func (source *Source) SkipVerify() bool {
return ok && skipVerifiable.IsSkipVerify()
}
func (source *Source) TwoFactorShouldSkip() bool {
return source.TwoFactorPolicy == "skip"
}
// CreateSource inserts a AuthSource in the DB if not already
// existing with the given name.
func CreateSource(ctx context.Context, source *Source) error {
@@ -223,9 +230,7 @@ func CreateSource(ctx context.Context, source *Source) error {
return nil
}
if settable, ok := source.Cfg.(SourceSettable); ok {
settable.SetAuthSource(source)
}
source.Cfg.SetAuthSource(source)
registerableSource, ok := source.Cfg.(RegisterableSource)
if !ok {
@@ -320,9 +325,7 @@ func UpdateSource(ctx context.Context, source *Source) error {
return nil
}
if settable, ok := source.Cfg.(SourceSettable); ok {
settable.SetAuthSource(source)
}
source.Cfg.SetAuthSource(source)
registerableSource, ok := source.Cfg.(RegisterableSource)
if !ok {
@@ -331,7 +334,7 @@ func UpdateSource(ctx context.Context, source *Source) error {
err = registerableSource.RegisterSource()
if err != nil {
// restore original values since we cannot update the provider it self
// restore original values since we cannot update the provider itself
if _, err := db.GetEngine(ctx).ID(source.ID).AllCols().Update(originalSource); err != nil {
log.Error("UpdateSource: Error while wrapOpenIDConnectInitializeError: %v", err)
}
+8 -5
View File
@@ -8,15 +8,17 @@ import (
"testing"
auth_model "code.gitea.io/gitea/models/auth"
"code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/models/unittest"
"code.gitea.io/gitea/modules/json"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"xorm.io/xorm/schemas"
)
type TestSource struct {
auth_model.ConfigBase
Provider string
ClientID string
ClientSecret string
@@ -37,12 +39,12 @@ func (source *TestSource) ToDB() ([]byte, error) {
func TestDumpAuthSource(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
authSourceSchema, err := db.TableInfo(new(auth_model.Source))
authSourceSchema, err := unittest.GetXORMEngine().TableInfo(new(auth_model.Source))
assert.NoError(t, err)
auth_model.RegisterTypeConfig(auth_model.OAuth2, new(TestSource))
auth_model.CreateSource(db.DefaultContext, &auth_model.Source{
auth_model.CreateSource(t.Context(), &auth_model.Source{
Type: auth_model.OAuth2,
Name: "TestSource",
IsActive: false,
@@ -54,7 +56,8 @@ func TestDumpAuthSource(t *testing.T) {
sb := new(strings.Builder)
db.DumpTables([]*schemas.Table{authSourceSchema}, sb)
// TODO: this test is quite hacky, it should use a low-level "select" (without model processors) but not a database dump
engine := unittest.GetXORMEngine()
require.NoError(t, engine.DumpTables([]*schemas.Table{authSourceSchema}, sb))
assert.Contains(t, sb.String(), `"Provider":"ConvertibleSourceName"`)
}
+10
View File
@@ -164,3 +164,13 @@ func DeleteTwoFactorByID(ctx context.Context, id, userID int64) error {
}
return nil
}
func HasTwoFactorOrWebAuthn(ctx context.Context, id int64) (bool, error) {
has, err := HasTwoFactorByUID(ctx, id)
if err != nil {
return false, err
} else if has {
return true, nil
}
return HasWebAuthnRegistrationsByUID(ctx, id)
}
+9 -10
View File
@@ -7,7 +7,6 @@ import (
"testing"
auth_model "code.gitea.io/gitea/models/auth"
"code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/models/unittest"
"github.com/go-webauthn/webauthn/webauthn"
@@ -17,11 +16,11 @@ import (
func TestGetWebAuthnCredentialByID(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
res, err := auth_model.GetWebAuthnCredentialByID(db.DefaultContext, 1)
res, err := auth_model.GetWebAuthnCredentialByID(t.Context(), 1)
assert.NoError(t, err)
assert.Equal(t, "WebAuthn credential", res.Name)
_, err = auth_model.GetWebAuthnCredentialByID(db.DefaultContext, 342432)
_, err = auth_model.GetWebAuthnCredentialByID(t.Context(), 342432)
assert.Error(t, err)
assert.True(t, auth_model.IsErrWebAuthnCredentialNotExist(err))
}
@@ -29,7 +28,7 @@ func TestGetWebAuthnCredentialByID(t *testing.T) {
func TestGetWebAuthnCredentialsByUID(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
res, err := auth_model.GetWebAuthnCredentialsByUID(db.DefaultContext, 32)
res, err := auth_model.GetWebAuthnCredentialsByUID(t.Context(), 32)
assert.NoError(t, err)
assert.Len(t, res, 1)
assert.Equal(t, "WebAuthn credential", res[0].Name)
@@ -43,25 +42,25 @@ func TestWebAuthnCredential_UpdateSignCount(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
cred := unittest.AssertExistsAndLoadBean(t, &auth_model.WebAuthnCredential{ID: 1})
cred.SignCount = 1
assert.NoError(t, cred.UpdateSignCount(db.DefaultContext))
unittest.AssertExistsIf(t, true, &auth_model.WebAuthnCredential{ID: 1, SignCount: 1})
assert.NoError(t, cred.UpdateSignCount(t.Context()))
unittest.AssertExistsAndLoadBean(t, &auth_model.WebAuthnCredential{ID: 1, SignCount: 1})
}
func TestWebAuthnCredential_UpdateLargeCounter(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
cred := unittest.AssertExistsAndLoadBean(t, &auth_model.WebAuthnCredential{ID: 1})
cred.SignCount = 0xffffffff
assert.NoError(t, cred.UpdateSignCount(db.DefaultContext))
unittest.AssertExistsIf(t, true, &auth_model.WebAuthnCredential{ID: 1, SignCount: 0xffffffff})
assert.NoError(t, cred.UpdateSignCount(t.Context()))
unittest.AssertExistsAndLoadBean(t, &auth_model.WebAuthnCredential{ID: 1, SignCount: 0xffffffff})
}
func TestCreateCredential(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
res, err := auth_model.CreateCredential(db.DefaultContext, 1, "WebAuthn Created Credential", &webauthn.Credential{ID: []byte("Test")})
res, err := auth_model.CreateCredential(t.Context(), 1, "WebAuthn Created Credential", &webauthn.Credential{ID: []byte("Test")})
assert.NoError(t, err)
assert.Equal(t, "WebAuthn Created Credential", res.Name)
assert.Equal(t, []byte("Test"), res.CredentialID)
unittest.AssertExistsIf(t, true, &auth_model.WebAuthnCredential{Name: "WebAuthn Created Credential", UserID: 1})
unittest.AssertExistsAndLoadBean(t, &auth_model.WebAuthnCredential{Name: "WebAuthn Created Credential", UserID: 1})
}