all: unwrap `database.AccessTokensStore` interface ()

pull/7676/head
Joe Chen 2024-02-20 21:47:32 -05:00 committed by GitHub
parent 917c14f2ce
commit 8054ffc12f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
22 changed files with 752 additions and 892 deletions

View File

@ -62,7 +62,7 @@ jobs:
name: Test name: Test
strategy: strategy:
matrix: matrix:
go-version: [ 1.21.x, 1.22.x ] go-version: [ 1.22.x ]
platform: [ ubuntu-latest, macos-latest ] platform: [ ubuntu-latest, macos-latest ]
runs-on: ${{ matrix.platform }} runs-on: ${{ matrix.platform }}
steps: steps:
@ -102,7 +102,7 @@ jobs:
name: Test Windows name: Test Windows
strategy: strategy:
matrix: matrix:
go-version: [ 1.21.x, 1.22.x ] go-version: [ 1.22.x ]
platform: [ windows-latest ] platform: [ windows-latest ]
runs-on: ${{ matrix.platform }} runs-on: ${{ matrix.platform }}
steps: steps:
@ -140,7 +140,7 @@ jobs:
name: Postgres name: Postgres
strategy: strategy:
matrix: matrix:
go-version: [ 1.21.x, 1.22.x ] go-version: [ 1.22.x ]
platform: [ ubuntu-latest ] platform: [ ubuntu-latest ]
runs-on: ${{ matrix.platform }} runs-on: ${{ matrix.platform }}
services: services:
@ -176,7 +176,7 @@ jobs:
name: MySQL name: MySQL
strategy: strategy:
matrix: matrix:
go-version: [ 1.21.x, 1.22.x ] go-version: [ 1.22.x ]
platform: [ ubuntu-20.04 ] platform: [ ubuntu-20.04 ]
runs-on: ${{ matrix.platform }} runs-on: ${{ matrix.platform }}
steps: steps:
@ -201,7 +201,7 @@ jobs:
name: SQLite - Go name: SQLite - Go
strategy: strategy:
matrix: matrix:
go-version: [ 1.21.x, 1.22.x ] go-version: [ 1.22.x ]
platform: [ ubuntu-latest ] platform: [ ubuntu-latest ]
runs-on: ${{ matrix.platform }} runs-on: ${{ matrix.platform }}
steps: steps:

View File

@ -237,9 +237,11 @@ func runWeb(c *cli.Context) error {
m.Get("", user.SettingsOrganizations) m.Get("", user.SettingsOrganizations)
m.Post("/leave", user.SettingsLeaveOrganization) m.Post("/leave", user.SettingsLeaveOrganization)
}) })
m.Combo("/applications").Get(user.SettingsApplications).
Post(bindIgnErr(form.NewAccessToken{}), user.SettingsApplicationsPost) settingsHandler := user.NewSettingsHandler(user.NewSettingsStore())
m.Post("/applications/delete", user.SettingsDeleteApplication) m.Combo("/applications").Get(settingsHandler.Applications()).
Post(bindIgnErr(form.NewAccessToken{}), settingsHandler.ApplicationsPost())
m.Post("/applications/delete", settingsHandler.DeleteApplication())
m.Route("/delete", "GET,POST", user.SettingsDelete) m.Route("/delete", "GET,POST", user.SettingsDelete)
}, reqSignIn, func(c *context.Context) { }, reqSignIn, func(c *context.Context) {
c.Data["PageIsUserSettings"] = true c.Data["PageIsUserSettings"] = true
@ -652,7 +654,7 @@ func runWeb(c *cli.Context) error {
SetCookie: true, SetCookie: true,
Secure: conf.Server.URL.Scheme == "https", Secure: conf.Server.URL.Scheme == "https",
}), }),
context.Contexter(), context.Contexter(context.NewStore()),
) )
// *************************** // ***************************
@ -666,7 +668,7 @@ func runWeb(c *cli.Context) error {
lfs.RegisterRoutes(m.Router) lfs.RegisterRoutes(m.Router)
}) })
m.Route("/*", "GET,POST,OPTIONS", context.ServeGoGet(), repo.HTTPContexter(), repo.HTTP) m.Route("/*", "GET,POST,OPTIONS", context.ServeGoGet(), repo.HTTPContexter(repo.NewStore()), repo.HTTP)
}) })
// *************************** // ***************************

View File

@ -106,9 +106,18 @@ func isAPIPath(url string) bool {
return strings.HasPrefix(url, "/api/") return strings.HasPrefix(url, "/api/")
} }
type AuthStore interface {
// GetAccessTokenBySHA1 returns the access token with given SHA1. It returns
// database.ErrAccessTokenNotExist when not found.
GetAccessTokenBySHA1(ctx context.Context, sha1 string) (*database.AccessToken, error)
// TouchAccessTokenByID updates the updated time of the given access token to
// the current time.
TouchAccessTokenByID(ctx context.Context, id int64) error
}
// authenticatedUserID returns the ID of the authenticated user, along with a bool value // authenticatedUserID returns the ID of the authenticated user, along with a bool value
// which indicates whether the user uses token authentication. // which indicates whether the user uses token authentication.
func authenticatedUserID(c *macaron.Context, sess session.Store) (_ int64, isTokenAuth bool) { func authenticatedUserID(store AuthStore, c *macaron.Context, sess session.Store) (_ int64, isTokenAuth bool) {
if !database.HasEngine { if !database.HasEngine {
return 0, false return 0, false
} }
@ -132,14 +141,14 @@ func authenticatedUserID(c *macaron.Context, sess session.Store) (_ int64, isTok
// Let's see if token is valid. // Let's see if token is valid.
if len(tokenSHA) > 0 { if len(tokenSHA) > 0 {
t, err := database.AccessTokens.GetBySHA1(c.Req.Context(), tokenSHA) t, err := store.GetAccessTokenBySHA1(c.Req.Context(), tokenSHA)
if err != nil { if err != nil {
if !database.IsErrAccessTokenNotExist(err) { if !database.IsErrAccessTokenNotExist(err) {
log.Error("GetAccessTokenBySHA: %v", err) log.Error("GetAccessTokenBySHA: %v", err)
} }
return 0, false return 0, false
} }
if err = database.AccessTokens.Touch(c.Req.Context(), t.ID); err != nil { if err = store.TouchAccessTokenByID(c.Req.Context(), t.ID); err != nil {
log.Error("Failed to touch access token: %v", err) log.Error("Failed to touch access token: %v", err)
} }
return t.UserID, true return t.UserID, true
@ -165,12 +174,12 @@ func authenticatedUserID(c *macaron.Context, sess session.Store) (_ int64, isTok
// authenticatedUser returns the user object of the authenticated user, along with two bool values // authenticatedUser returns the user object of the authenticated user, along with two bool values
// which indicate whether the user uses HTTP Basic Authentication or token authentication respectively. // which indicate whether the user uses HTTP Basic Authentication or token authentication respectively.
func authenticatedUser(ctx *macaron.Context, sess session.Store) (_ *database.User, isBasicAuth, isTokenAuth bool) { func authenticatedUser(store AuthStore, ctx *macaron.Context, sess session.Store) (_ *database.User, isBasicAuth, isTokenAuth bool) {
if !database.HasEngine { if !database.HasEngine {
return nil, false, false return nil, false, false
} }
uid, isTokenAuth := authenticatedUserID(ctx, sess) uid, isTokenAuth := authenticatedUserID(store, ctx, sess)
if uid <= 0 { if uid <= 0 {
if conf.Auth.EnableReverseProxyAuthentication { if conf.Auth.EnableReverseProxyAuthentication {
@ -235,12 +244,12 @@ func authenticatedUser(ctx *macaron.Context, sess session.Store) (_ *database.Us
// AuthenticateByToken attempts to authenticate a user by the given access // AuthenticateByToken attempts to authenticate a user by the given access
// token. It returns database.ErrAccessTokenNotExist when the access token does not // token. It returns database.ErrAccessTokenNotExist when the access token does not
// exist. // exist.
func AuthenticateByToken(ctx context.Context, token string) (*database.User, error) { func AuthenticateByToken(store AuthStore, ctx context.Context, token string) (*database.User, error) {
t, err := database.AccessTokens.GetBySHA1(ctx, token) t, err := store.GetAccessTokenBySHA1(ctx, token)
if err != nil { if err != nil {
return nil, errors.Wrap(err, "get access token by SHA1") return nil, errors.Wrap(err, "get access token by SHA1")
} }
if err = database.AccessTokens.Touch(ctx, t.ID); err != nil { if err = store.TouchAccessTokenByID(ctx, t.ID); err != nil {
// NOTE: There is no need to fail the auth flow if we can't touch the token. // NOTE: There is no need to fail the auth flow if we can't touch the token.
log.Error("Failed to touch access token [id: %d]: %v", t.ID, err) log.Error("Failed to touch access token [id: %d]: %v", t.ID, err)
} }

View File

@ -235,7 +235,7 @@ func (c *Context) ServeContent(name string, r io.ReadSeeker, params ...any) {
var csrfTokenExcludePattern = lazyregexp.New(`[^a-zA-Z0-9-_].*`) var csrfTokenExcludePattern = lazyregexp.New(`[^a-zA-Z0-9-_].*`)
// Contexter initializes a classic context for a request. // Contexter initializes a classic context for a request.
func Contexter() macaron.Handler { func Contexter(store Store) macaron.Handler {
return func(ctx *macaron.Context, l i18n.Locale, cache cache.Cache, sess session.Store, f *session.Flash, x csrf.CSRF) { return func(ctx *macaron.Context, l i18n.Locale, cache cache.Cache, sess session.Store, f *session.Flash, x csrf.CSRF) {
c := &Context{ c := &Context{
Context: ctx, Context: ctx,
@ -260,7 +260,7 @@ func Contexter() macaron.Handler {
} }
// Get user from session or header when possible // Get user from session or header when possible
c.User, c.IsBasicAuth, c.IsTokenAuth = authenticatedUser(c.Context, c.Session) c.User, c.IsBasicAuth, c.IsTokenAuth = authenticatedUser(store, c.Context, c.Session)
if c.User != nil { if c.User != nil {
c.IsLogged = true c.IsLogged = true

34
internal/context/store.go Normal file
View File

@ -0,0 +1,34 @@
package context
import (
"context"
"gogs.io/gogs/internal/database"
)
// Store is the data layer carrier for context middleware. This interface is
// meant to abstract away and limit the exposure of the underlying data layer to
// the handler through a thin-wrapper.
type Store interface {
// GetAccessTokenBySHA1 returns the access token with given SHA1. It returns
// database.ErrAccessTokenNotExist when not found.
GetAccessTokenBySHA1(ctx context.Context, sha1 string) (*database.AccessToken, error)
// TouchAccessTokenByID updates the updated time of the given access token to
// the current time.
TouchAccessTokenByID(ctx context.Context, id int64) error
}
type store struct{}
// NewStore returns a new Store using the global database handle.
func NewStore() Store {
return &store{}
}
func (*store) GetAccessTokenBySHA1(ctx context.Context, sha1 string) (*database.AccessToken, error) {
return database.Handle.AccessTokens().GetBySHA1(ctx, sha1)
}
func (*store) TouchAccessTokenByID(ctx context.Context, id int64) error {
return database.Handle.AccessTokens().Touch(ctx, id)
}

View File

@ -17,28 +17,6 @@ import (
"gogs.io/gogs/internal/errutil" "gogs.io/gogs/internal/errutil"
) )
// AccessTokensStore is the persistent interface for access tokens.
type AccessTokensStore interface {
// Create creates a new access token and persist to database. It returns
// ErrAccessTokenAlreadyExist when an access token with same name already exists
// for the user.
Create(ctx context.Context, userID int64, name string) (*AccessToken, error)
// DeleteByID deletes the access token by given ID.
//
// 🚨 SECURITY: The "userID" is required to prevent attacker deletes arbitrary
// access token that belongs to another user.
DeleteByID(ctx context.Context, userID, id int64) error
// GetBySHA1 returns the access token with given SHA1. It returns
// ErrAccessTokenNotExist when not found.
GetBySHA1(ctx context.Context, sha1 string) (*AccessToken, error)
// List returns all access tokens belongs to given user.
List(ctx context.Context, userID int64) ([]*AccessToken, error)
// Touch updates the updated time of the given access token to the current time.
Touch(ctx context.Context, id int64) error
}
var AccessTokens AccessTokensStore
// AccessToken is a personal access token. // AccessToken is a personal access token.
type AccessToken struct { type AccessToken struct {
ID int64 `gorm:"primarykey"` ID int64 `gorm:"primarykey"`
@ -74,10 +52,13 @@ func (t *AccessToken) AfterFind(tx *gorm.DB) error {
return nil return nil
} }
var _ AccessTokensStore = (*accessTokensStore)(nil) // AccessTokensStore is the storage layer for access tokens.
type AccessTokensStore struct {
db *gorm.DB
}
type accessTokensStore struct { func newAccessTokensStore(db *gorm.DB) *AccessTokensStore {
*gorm.DB return &AccessTokensStore{db}
} }
type ErrAccessTokenAlreadyExist struct { type ErrAccessTokenAlreadyExist struct {
@ -85,19 +66,21 @@ type ErrAccessTokenAlreadyExist struct {
} }
func IsErrAccessTokenAlreadyExist(err error) bool { func IsErrAccessTokenAlreadyExist(err error) bool {
_, ok := err.(ErrAccessTokenAlreadyExist) return errors.As(err, &ErrAccessTokenAlreadyExist{})
return ok
} }
func (err ErrAccessTokenAlreadyExist) Error() string { func (err ErrAccessTokenAlreadyExist) Error() string {
return fmt.Sprintf("access token already exists: %v", err.args) return fmt.Sprintf("access token already exists: %v", err.args)
} }
func (s *accessTokensStore) Create(ctx context.Context, userID int64, name string) (*AccessToken, error) { // Create creates a new access token and persist to database. It returns
err := s.WithContext(ctx).Where("uid = ? AND name = ?", userID, name).First(new(AccessToken)).Error // ErrAccessTokenAlreadyExist when an access token with same name already exists
// for the user.
func (s *AccessTokensStore) Create(ctx context.Context, userID int64, name string) (*AccessToken, error) {
err := s.db.WithContext(ctx).Where("uid = ? AND name = ?", userID, name).First(new(AccessToken)).Error
if err == nil { if err == nil {
return nil, ErrAccessTokenAlreadyExist{args: errutil.Args{"userID": userID, "name": name}} return nil, ErrAccessTokenAlreadyExist{args: errutil.Args{"userID": userID, "name": name}}
} else if err != gorm.ErrRecordNotFound { } else if !errors.Is(err, gorm.ErrRecordNotFound) {
return nil, err return nil, err
} }
@ -110,7 +93,7 @@ func (s *accessTokensStore) Create(ctx context.Context, userID int64, name strin
Sha1: sha256[:40], // To pass the column unique constraint, keep the length of SHA1. Sha1: sha256[:40], // To pass the column unique constraint, keep the length of SHA1.
SHA256: sha256, SHA256: sha256,
} }
if err = s.WithContext(ctx).Create(accessToken).Error; err != nil { if err = s.db.WithContext(ctx).Create(accessToken).Error; err != nil {
return nil, err return nil, err
} }
@ -119,8 +102,12 @@ func (s *accessTokensStore) Create(ctx context.Context, userID int64, name strin
return accessToken, nil return accessToken, nil
} }
func (s *accessTokensStore) DeleteByID(ctx context.Context, userID, id int64) error { // DeleteByID deletes the access token by given ID.
return s.WithContext(ctx).Where("id = ? AND uid = ?", id, userID).Delete(new(AccessToken)).Error //
// 🚨 SECURITY: The "userID" is required to prevent attacker deletes arbitrary
// access token that belongs to another user.
func (s *AccessTokensStore) DeleteByID(ctx context.Context, userID, id int64) error {
return s.db.WithContext(ctx).Where("id = ? AND uid = ?", id, userID).Delete(new(AccessToken)).Error
} }
var _ errutil.NotFound = (*ErrAccessTokenNotExist)(nil) var _ errutil.NotFound = (*ErrAccessTokenNotExist)(nil)
@ -132,8 +119,7 @@ type ErrAccessTokenNotExist struct {
// IsErrAccessTokenNotExist returns true if the underlying error has the type // IsErrAccessTokenNotExist returns true if the underlying error has the type
// ErrAccessTokenNotExist. // ErrAccessTokenNotExist.
func IsErrAccessTokenNotExist(err error) bool { func IsErrAccessTokenNotExist(err error) bool {
_, ok := errors.Cause(err).(ErrAccessTokenNotExist) return errors.As(errors.Cause(err), &ErrAccessTokenNotExist{})
return ok
} }
func (err ErrAccessTokenNotExist) Error() string { func (err ErrAccessTokenNotExist) Error() string {
@ -144,7 +130,9 @@ func (ErrAccessTokenNotExist) NotFound() bool {
return true return true
} }
func (s *accessTokensStore) GetBySHA1(ctx context.Context, sha1 string) (*AccessToken, error) { // GetBySHA1 returns the access token with given SHA1. It returns
// ErrAccessTokenNotExist when not found.
func (s *AccessTokensStore) GetBySHA1(ctx context.Context, sha1 string) (*AccessToken, error) {
// No need to waste a query for an empty SHA1. // No need to waste a query for an empty SHA1.
if sha1 == "" { if sha1 == "" {
return nil, ErrAccessTokenNotExist{args: errutil.Args{"sha": sha1}} return nil, ErrAccessTokenNotExist{args: errutil.Args{"sha": sha1}}
@ -152,25 +140,26 @@ func (s *accessTokensStore) GetBySHA1(ctx context.Context, sha1 string) (*Access
sha256 := cryptoutil.SHA256(sha1) sha256 := cryptoutil.SHA256(sha1)
token := new(AccessToken) token := new(AccessToken)
err := s.WithContext(ctx).Where("sha256 = ?", sha256).First(token).Error err := s.db.WithContext(ctx).Where("sha256 = ?", sha256).First(token).Error
if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) {
if err == gorm.ErrRecordNotFound { return nil, ErrAccessTokenNotExist{args: errutil.Args{"sha": sha1}}
return nil, ErrAccessTokenNotExist{args: errutil.Args{"sha": sha1}} } else if err != nil {
}
return nil, err return nil, err
} }
return token, nil return token, nil
} }
func (s *accessTokensStore) List(ctx context.Context, userID int64) ([]*AccessToken, error) { // List returns all access tokens belongs to given user.
func (s *AccessTokensStore) List(ctx context.Context, userID int64) ([]*AccessToken, error) {
var tokens []*AccessToken var tokens []*AccessToken
return tokens, s.WithContext(ctx).Where("uid = ?", userID).Order("id ASC").Find(&tokens).Error return tokens, s.db.WithContext(ctx).Where("uid = ?", userID).Order("id ASC").Find(&tokens).Error
} }
func (s *accessTokensStore) Touch(ctx context.Context, id int64) error { // Touch updates the updated time of the given access token to the current time.
return s.WithContext(ctx). func (s *AccessTokensStore) Touch(ctx context.Context, id int64) error {
return s.db.WithContext(ctx).
Model(new(AccessToken)). Model(new(AccessToken)).
Where("id = ?", id). Where("id = ?", id).
UpdateColumn("updated_unix", s.NowFunc().Unix()). UpdateColumn("updated_unix", s.db.NowFunc().Unix()).
Error Error
} }

View File

@ -98,13 +98,13 @@ func TestAccessTokens(t *testing.T) {
t.Parallel() t.Parallel()
ctx := context.Background() ctx := context.Background()
db := &accessTokensStore{ s := &AccessTokensStore{
DB: newTestDB(t, "accessTokensStore"), db: newTestDB(t, "AccessTokensStore"),
} }
for _, tc := range []struct { for _, tc := range []struct {
name string name string
test func(t *testing.T, ctx context.Context, db *accessTokensStore) test func(t *testing.T, ctx context.Context, s *AccessTokensStore)
}{ }{
{"Create", accessTokensCreate}, {"Create", accessTokensCreate},
{"DeleteByID", accessTokensDeleteByID}, {"DeleteByID", accessTokensDeleteByID},
@ -114,10 +114,10 @@ func TestAccessTokens(t *testing.T) {
} { } {
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
t.Cleanup(func() { t.Cleanup(func() {
err := clearTables(t, db.DB) err := clearTables(t, s.db)
require.NoError(t, err) require.NoError(t, err)
}) })
tc.test(t, ctx, db) tc.test(t, ctx, s)
}) })
if t.Failed() { if t.Failed() {
break break
@ -125,9 +125,9 @@ func TestAccessTokens(t *testing.T) {
} }
} }
func accessTokensCreate(t *testing.T, ctx context.Context, db *accessTokensStore) { func accessTokensCreate(t *testing.T, ctx context.Context, s *AccessTokensStore) {
// Create first access token with name "Test" // Create first access token with name "Test"
token, err := db.Create(ctx, 1, "Test") token, err := s.Create(ctx, 1, "Test")
require.NoError(t, err) require.NoError(t, err)
assert.Equal(t, int64(1), token.UserID) assert.Equal(t, int64(1), token.UserID)
@ -135,12 +135,12 @@ func accessTokensCreate(t *testing.T, ctx context.Context, db *accessTokensStore
assert.Equal(t, 40, len(token.Sha1), "sha1 length") assert.Equal(t, 40, len(token.Sha1), "sha1 length")
// Get it back and check the Created field // Get it back and check the Created field
token, err = db.GetBySHA1(ctx, token.Sha1) token, err = s.GetBySHA1(ctx, token.Sha1)
require.NoError(t, err) require.NoError(t, err)
assert.Equal(t, db.NowFunc().Format(time.RFC3339), token.Created.UTC().Format(time.RFC3339)) assert.Equal(t, s.db.NowFunc().Format(time.RFC3339), token.Created.UTC().Format(time.RFC3339))
// Try create second access token with same name should fail // Try create second access token with same name should fail
_, err = db.Create(ctx, token.UserID, token.Name) _, err = s.Create(ctx, token.UserID, token.Name)
wantErr := ErrAccessTokenAlreadyExist{ wantErr := ErrAccessTokenAlreadyExist{
args: errutil.Args{ args: errutil.Args{
"userID": token.UserID, "userID": token.UserID,
@ -150,25 +150,25 @@ func accessTokensCreate(t *testing.T, ctx context.Context, db *accessTokensStore
assert.Equal(t, wantErr, err) assert.Equal(t, wantErr, err)
} }
func accessTokensDeleteByID(t *testing.T, ctx context.Context, db *accessTokensStore) { func accessTokensDeleteByID(t *testing.T, ctx context.Context, s *AccessTokensStore) {
// Create an access token with name "Test" // Create an access token with name "Test"
token, err := db.Create(ctx, 1, "Test") token, err := s.Create(ctx, 1, "Test")
require.NoError(t, err) require.NoError(t, err)
// Delete a token with mismatched user ID is noop // Delete a token with mismatched user ID is noop
err = db.DeleteByID(ctx, 2, token.ID) err = s.DeleteByID(ctx, 2, token.ID)
require.NoError(t, err) require.NoError(t, err)
// We should be able to get it back // We should be able to get it back
_, err = db.GetBySHA1(ctx, token.Sha1) _, err = s.GetBySHA1(ctx, token.Sha1)
require.NoError(t, err) require.NoError(t, err)
// Now delete this token with correct user ID // Now delete this token with correct user ID
err = db.DeleteByID(ctx, token.UserID, token.ID) err = s.DeleteByID(ctx, token.UserID, token.ID)
require.NoError(t, err) require.NoError(t, err)
// We should get token not found error // We should get token not found error
_, err = db.GetBySHA1(ctx, token.Sha1) _, err = s.GetBySHA1(ctx, token.Sha1)
wantErr := ErrAccessTokenNotExist{ wantErr := ErrAccessTokenNotExist{
args: errutil.Args{ args: errutil.Args{
"sha": token.Sha1, "sha": token.Sha1,
@ -177,17 +177,17 @@ func accessTokensDeleteByID(t *testing.T, ctx context.Context, db *accessTokensS
assert.Equal(t, wantErr, err) assert.Equal(t, wantErr, err)
} }
func accessTokensGetBySHA(t *testing.T, ctx context.Context, db *accessTokensStore) { func accessTokensGetBySHA(t *testing.T, ctx context.Context, s *AccessTokensStore) {
// Create an access token with name "Test" // Create an access token with name "Test"
token, err := db.Create(ctx, 1, "Test") token, err := s.Create(ctx, 1, "Test")
require.NoError(t, err) require.NoError(t, err)
// We should be able to get it back // We should be able to get it back
_, err = db.GetBySHA1(ctx, token.Sha1) _, err = s.GetBySHA1(ctx, token.Sha1)
require.NoError(t, err) require.NoError(t, err)
// Try to get a non-existent token // Try to get a non-existent token
_, err = db.GetBySHA1(ctx, "bad_sha") _, err = s.GetBySHA1(ctx, "bad_sha")
wantErr := ErrAccessTokenNotExist{ wantErr := ErrAccessTokenNotExist{
args: errutil.Args{ args: errutil.Args{
"sha": "bad_sha", "sha": "bad_sha",
@ -196,21 +196,21 @@ func accessTokensGetBySHA(t *testing.T, ctx context.Context, db *accessTokensSto
assert.Equal(t, wantErr, err) assert.Equal(t, wantErr, err)
} }
func accessTokensList(t *testing.T, ctx context.Context, db *accessTokensStore) { func accessTokensList(t *testing.T, ctx context.Context, s *AccessTokensStore) {
// Create two access tokens for user 1 // Create two access tokens for user 1
_, err := db.Create(ctx, 1, "user1_1") _, err := s.Create(ctx, 1, "user1_1")
require.NoError(t, err) require.NoError(t, err)
_, err = db.Create(ctx, 1, "user1_2") _, err = s.Create(ctx, 1, "user1_2")
require.NoError(t, err) require.NoError(t, err)
// Create one access token for user 2 // Create one access token for user 2
_, err = db.Create(ctx, 2, "user2_1") _, err = s.Create(ctx, 2, "user2_1")
require.NoError(t, err) require.NoError(t, err)
// List all access tokens for user 1 // List all access tokens for user 1
tokens, err := db.List(ctx, 1) tokens, err := s.List(ctx, 1)
require.NoError(t, err) require.NoError(t, err)
assert.Equal(t, 2, len(tokens), "number of tokens") require.Equal(t, 2, len(tokens), "number of tokens")
assert.Equal(t, int64(1), tokens[0].UserID) assert.Equal(t, int64(1), tokens[0].UserID)
assert.Equal(t, "user1_1", tokens[0].Name) assert.Equal(t, "user1_1", tokens[0].Name)
@ -219,19 +219,19 @@ func accessTokensList(t *testing.T, ctx context.Context, db *accessTokensStore)
assert.Equal(t, "user1_2", tokens[1].Name) assert.Equal(t, "user1_2", tokens[1].Name)
} }
func accessTokensTouch(t *testing.T, ctx context.Context, db *accessTokensStore) { func accessTokensTouch(t *testing.T, ctx context.Context, s *AccessTokensStore) {
// Create an access token with name "Test" // Create an access token with name "Test"
token, err := db.Create(ctx, 1, "Test") token, err := s.Create(ctx, 1, "Test")
require.NoError(t, err) require.NoError(t, err)
// Updated field is zero now // Updated field is zero now
assert.True(t, token.Updated.IsZero()) assert.True(t, token.Updated.IsZero())
err = db.Touch(ctx, token.ID) err = s.Touch(ctx, token.ID)
require.NoError(t, err) require.NoError(t, err)
// Get back from DB should have Updated set // Get back from DB should have Updated set
token, err = db.GetBySHA1(ctx, token.Sha1) token, err = s.GetBySHA1(ctx, token.Sha1)
require.NoError(t, err) require.NoError(t, err)
assert.Equal(t, db.NowFunc().Format(time.RFC3339), token.Updated.UTC().Format(time.RFC3339)) assert.Equal(t, s.db.NowFunc().Format(time.RFC3339), token.Updated.UTC().Format(time.RFC3339))
} }

View File

@ -50,8 +50,8 @@ var Tables = []any{
new(Notice), new(Notice),
} }
// Init initializes the database with given logger. // NewConnection returns a new database connection with the given logger.
func Init(w logger.Writer) (*gorm.DB, error) { func NewConnection(w logger.Writer) (*gorm.DB, error) {
level := logger.Info level := logger.Info
if conf.IsProdMode() { if conf.IsProdMode() {
level = logger.Warn level = logger.Warn
@ -123,7 +123,6 @@ func Init(w logger.Writer) (*gorm.DB, error) {
} }
// Initialize stores, sorted in alphabetical order. // Initialize stores, sorted in alphabetical order.
AccessTokens = &accessTokensStore{DB: db}
Actions = NewActionsStore(db) Actions = NewActionsStore(db)
LoginSources = &loginSourcesStore{DB: db, files: sourceFiles} LoginSources = &loginSourcesStore{DB: db, files: sourceFiles}
LFS = &lfsStore{DB: db} LFS = &lfsStore{DB: db}
@ -136,3 +135,28 @@ func Init(w logger.Writer) (*gorm.DB, error) {
return db, nil return db, nil
} }
type DB struct {
db *gorm.DB
}
// Handle is the global database handle. It could be `nil` during the
// installation mode.
//
// NOTE: Because we need to register all the routes even during the installation
// mode (which initially has no database configuration), we have to use a global
// variable since we can't pass a database handler around before it's available.
//
// NOTE: It is not guarded by a mutex because it is only written once either
// during the service start or during the installation process (which is a
// single-thread process).
var Handle *DB
// SetHandle updates the global database handle with the given connection.
func SetHandle(db *gorm.DB) {
Handle = &DB{db: db}
}
func (db *DB) AccessTokens() *AccessTokensStore {
return newAccessTokensStore(db.db)
}

View File

@ -8,14 +8,6 @@ import (
"testing" "testing"
) )
func SetMockAccessTokensStore(t *testing.T, mock AccessTokensStore) {
before := AccessTokens
AccessTokens = mock
t.Cleanup(func() {
AccessTokens = before
})
}
func SetMockLFSStore(t *testing.T, mock LFSStore) { func SetMockLFSStore(t *testing.T, mock LFSStore) {
before := LFS before := LFS
LFS = mock LFS = mock

View File

@ -178,24 +178,24 @@ func SetEngine() (*gorm.DB, error) {
return nil, errors.Wrap(err, "new log writer") return nil, errors.Wrap(err, "new log writer")
} }
} }
return Init(gormLogger) return NewConnection(gormLogger)
} }
func NewEngine() (err error) { func NewEngine() (*gorm.DB, error) {
db, err := SetEngine() db, err := SetEngine()
if err != nil { if err != nil {
return err return nil, err
} }
if err = migrations.Migrate(db); err != nil { if err = migrations.Migrate(db); err != nil {
return fmt.Errorf("migrate: %v", err) return nil, fmt.Errorf("migrate: %v", err)
} }
if err = x.StoreEngine("InnoDB").Sync2(legacyTables...); err != nil { if err = x.StoreEngine("InnoDB").Sync2(legacyTables...); err != nil {
return errors.Wrap(err, "sync tables") return nil, errors.Wrap(err, "sync tables")
} }
return nil return db, nil
} }
type Statistic struct { type Statistic struct {

View File

@ -186,9 +186,10 @@ func RegisterRoutes(m *macaron.Macaron) {
m.Get("", user.GetInfo) m.Get("", user.GetInfo)
m.Group("/tokens", func() { m.Group("/tokens", func() {
accessTokensHandler := user.NewAccessTokensHandler(user.NewAccessTokensStore())
m.Combo(""). m.Combo("").
Get(user.ListAccessTokens). Get(accessTokensHandler.List()).
Post(bind(api.CreateAccessTokenOption{}), user.CreateAccessToken) Post(bind(api.CreateAccessTokenOption{}), accessTokensHandler.Create())
}, reqBasicAuth()) }, reqBasicAuth())
}) })
}) })

View File

@ -0,0 +1,88 @@
// Copyright 2014 The Gogs Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package user
import (
gocontext "context"
"net/http"
api "github.com/gogs/go-gogs-client"
"gopkg.in/macaron.v1"
"gogs.io/gogs/internal/context"
"gogs.io/gogs/internal/database"
)
// AccessTokensHandler is the handler for users access tokens API endpoints.
type AccessTokensHandler struct {
store AccessTokensStore
}
// NewAccessTokensHandler returns a new AccessTokensHandler for users access
// tokens API endpoints.
func NewAccessTokensHandler(s AccessTokensStore) *AccessTokensHandler {
return &AccessTokensHandler{
store: s,
}
}
func (h *AccessTokensHandler) List() macaron.Handler {
return func(c *context.APIContext) {
tokens, err := h.store.ListAccessTokens(c.Req.Context(), c.User.ID)
if err != nil {
c.Error(err, "list access tokens")
return
}
apiTokens := make([]*api.AccessToken, len(tokens))
for i := range tokens {
apiTokens[i] = &api.AccessToken{Name: tokens[i].Name, Sha1: tokens[i].Sha1}
}
c.JSONSuccess(&apiTokens)
}
}
func (h *AccessTokensHandler) Create() macaron.Handler {
return func(c *context.APIContext, form api.CreateAccessTokenOption) {
t, err := h.store.CreateAccessToken(c.Req.Context(), c.User.ID, form.Name)
if err != nil {
if database.IsErrAccessTokenAlreadyExist(err) {
c.ErrorStatus(http.StatusUnprocessableEntity, err)
} else {
c.Error(err, "new access token")
}
return
}
c.JSON(http.StatusCreated, &api.AccessToken{Name: t.Name, Sha1: t.Sha1})
}
}
// AccessTokensStore is the data layer carrier for user access tokens API
// endpoints. This interface is meant to abstract away and limit the exposure of
// the underlying data layer to the handler through a thin-wrapper.
type AccessTokensStore interface {
// CreateAccessToken creates a new access token and persist to database. It
// returns database.ErrAccessTokenAlreadyExist when an access token with same
// name already exists for the user.
CreateAccessToken(ctx gocontext.Context, userID int64, name string) (*database.AccessToken, error)
// ListAccessTokens returns all access tokens belongs to given user.
ListAccessTokens(ctx gocontext.Context, userID int64) ([]*database.AccessToken, error)
}
type accessTokensStore struct{}
// NewAccessTokensStore returns a new AccessTokensStore using the global
// database handle.
func NewAccessTokensStore() AccessTokensStore {
return &accessTokensStore{}
}
func (*accessTokensStore) CreateAccessToken(ctx gocontext.Context, userID int64, name string) (*database.AccessToken, error) {
return database.Handle.AccessTokens().Create(ctx, userID, name)
}
func (*accessTokensStore) ListAccessTokens(ctx gocontext.Context, userID int64) ([]*database.AccessToken, error) {
return database.Handle.AccessTokens().List(ctx, userID)
}

View File

@ -1,41 +0,0 @@
// Copyright 2014 The Gogs Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package user
import (
"net/http"
api "github.com/gogs/go-gogs-client"
"gogs.io/gogs/internal/context"
"gogs.io/gogs/internal/database"
)
func ListAccessTokens(c *context.APIContext) {
tokens, err := database.AccessTokens.List(c.Req.Context(), c.User.ID)
if err != nil {
c.Error(err, "list access tokens")
return
}
apiTokens := make([]*api.AccessToken, len(tokens))
for i := range tokens {
apiTokens[i] = &api.AccessToken{Name: tokens[i].Name, Sha1: tokens[i].Sha1}
}
c.JSONSuccess(&apiTokens)
}
func CreateAccessToken(c *context.APIContext, form api.CreateAccessTokenOption) {
t, err := database.AccessTokens.Create(c.Req.Context(), c.User.ID, form.Name)
if err != nil {
if database.IsErrAccessTokenAlreadyExist(err) {
c.ErrorStatus(http.StatusUnprocessableEntity, err)
} else {
c.Error(err, "new access token")
}
return
}
c.JSON(http.StatusCreated, &api.AccessToken{Name: t.Name, Sha1: t.Sha1})
}

View File

@ -71,9 +71,11 @@ func GlobalInit(customConf string) error {
if conf.Security.InstallLock { if conf.Security.InstallLock {
highlight.NewContext() highlight.NewContext()
markup.NewSanitizer() markup.NewSanitizer()
if err := database.NewEngine(); err != nil { db, err := database.NewEngine()
if err != nil {
log.Fatal("Failed to initialize ORM engine: %v", err) log.Fatal("Failed to initialize ORM engine: %v", err)
} }
database.SetHandle(db)
database.HasEngine = true database.HasEngine = true
database.LoadRepoConfig() database.LoadRepoConfig()

View File

@ -14,657 +14,6 @@ import (
lfsutil "gogs.io/gogs/internal/lfsutil" lfsutil "gogs.io/gogs/internal/lfsutil"
) )
// MockAccessTokensStore is a mock implementation of the AccessTokensStore
// interface (from the package gogs.io/gogs/internal/database) used for unit
// testing.
type MockAccessTokensStore struct {
// CreateFunc is an instance of a mock function object controlling the
// behavior of the method Create.
CreateFunc *AccessTokensStoreCreateFunc
// DeleteByIDFunc is an instance of a mock function object controlling
// the behavior of the method DeleteByID.
DeleteByIDFunc *AccessTokensStoreDeleteByIDFunc
// GetBySHA1Func is an instance of a mock function object controlling
// the behavior of the method GetBySHA1.
GetBySHA1Func *AccessTokensStoreGetBySHA1Func
// ListFunc is an instance of a mock function object controlling the
// behavior of the method List.
ListFunc *AccessTokensStoreListFunc
// TouchFunc is an instance of a mock function object controlling the
// behavior of the method Touch.
TouchFunc *AccessTokensStoreTouchFunc
}
// NewMockAccessTokensStore creates a new mock of the AccessTokensStore
// interface. All methods return zero values for all results, unless
// overwritten.
func NewMockAccessTokensStore() *MockAccessTokensStore {
return &MockAccessTokensStore{
CreateFunc: &AccessTokensStoreCreateFunc{
defaultHook: func(context.Context, int64, string) (r0 *database.AccessToken, r1 error) {
return
},
},
DeleteByIDFunc: &AccessTokensStoreDeleteByIDFunc{
defaultHook: func(context.Context, int64, int64) (r0 error) {
return
},
},
GetBySHA1Func: &AccessTokensStoreGetBySHA1Func{
defaultHook: func(context.Context, string) (r0 *database.AccessToken, r1 error) {
return
},
},
ListFunc: &AccessTokensStoreListFunc{
defaultHook: func(context.Context, int64) (r0 []*database.AccessToken, r1 error) {
return
},
},
TouchFunc: &AccessTokensStoreTouchFunc{
defaultHook: func(context.Context, int64) (r0 error) {
return
},
},
}
}
// NewStrictMockAccessTokensStore creates a new mock of the
// AccessTokensStore interface. All methods panic on invocation, unless
// overwritten.
func NewStrictMockAccessTokensStore() *MockAccessTokensStore {
return &MockAccessTokensStore{
CreateFunc: &AccessTokensStoreCreateFunc{
defaultHook: func(context.Context, int64, string) (*database.AccessToken, error) {
panic("unexpected invocation of MockAccessTokensStore.Create")
},
},
DeleteByIDFunc: &AccessTokensStoreDeleteByIDFunc{
defaultHook: func(context.Context, int64, int64) error {
panic("unexpected invocation of MockAccessTokensStore.DeleteByID")
},
},
GetBySHA1Func: &AccessTokensStoreGetBySHA1Func{
defaultHook: func(context.Context, string) (*database.AccessToken, error) {
panic("unexpected invocation of MockAccessTokensStore.GetBySHA1")
},
},
ListFunc: &AccessTokensStoreListFunc{
defaultHook: func(context.Context, int64) ([]*database.AccessToken, error) {
panic("unexpected invocation of MockAccessTokensStore.List")
},
},
TouchFunc: &AccessTokensStoreTouchFunc{
defaultHook: func(context.Context, int64) error {
panic("unexpected invocation of MockAccessTokensStore.Touch")
},
},
}
}
// NewMockAccessTokensStoreFrom creates a new mock of the
// MockAccessTokensStore interface. All methods delegate to the given
// implementation, unless overwritten.
func NewMockAccessTokensStoreFrom(i database.AccessTokensStore) *MockAccessTokensStore {
return &MockAccessTokensStore{
CreateFunc: &AccessTokensStoreCreateFunc{
defaultHook: i.Create,
},
DeleteByIDFunc: &AccessTokensStoreDeleteByIDFunc{
defaultHook: i.DeleteByID,
},
GetBySHA1Func: &AccessTokensStoreGetBySHA1Func{
defaultHook: i.GetBySHA1,
},
ListFunc: &AccessTokensStoreListFunc{
defaultHook: i.List,
},
TouchFunc: &AccessTokensStoreTouchFunc{
defaultHook: i.Touch,
},
}
}
// AccessTokensStoreCreateFunc describes the behavior when the Create method
// of the parent MockAccessTokensStore instance is invoked.
type AccessTokensStoreCreateFunc struct {
defaultHook func(context.Context, int64, string) (*database.AccessToken, error)
hooks []func(context.Context, int64, string) (*database.AccessToken, error)
history []AccessTokensStoreCreateFuncCall
mutex sync.Mutex
}
// Create delegates to the next hook function in the queue and stores the
// parameter and result values of this invocation.
func (m *MockAccessTokensStore) Create(v0 context.Context, v1 int64, v2 string) (*database.AccessToken, error) {
r0, r1 := m.CreateFunc.nextHook()(v0, v1, v2)
m.CreateFunc.appendCall(AccessTokensStoreCreateFuncCall{v0, v1, v2, r0, r1})
return r0, r1
}
// SetDefaultHook sets function that is called when the Create method of the
// parent MockAccessTokensStore instance is invoked and the hook queue is
// empty.
func (f *AccessTokensStoreCreateFunc) SetDefaultHook(hook func(context.Context, int64, string) (*database.AccessToken, error)) {
f.defaultHook = hook
}
// PushHook adds a function to the end of hook queue. Each invocation of the
// Create method of the parent MockAccessTokensStore instance invokes the
// hook at the front of the queue and discards it. After the queue is empty,
// the default hook function is invoked for any future action.
func (f *AccessTokensStoreCreateFunc) PushHook(hook func(context.Context, int64, string) (*database.AccessToken, error)) {
f.mutex.Lock()
f.hooks = append(f.hooks, hook)
f.mutex.Unlock()
}
// SetDefaultReturn calls SetDefaultHook with a function that returns the
// given values.
func (f *AccessTokensStoreCreateFunc) SetDefaultReturn(r0 *database.AccessToken, r1 error) {
f.SetDefaultHook(func(context.Context, int64, string) (*database.AccessToken, error) {
return r0, r1
})
}
// PushReturn calls PushHook with a function that returns the given values.
func (f *AccessTokensStoreCreateFunc) PushReturn(r0 *database.AccessToken, r1 error) {
f.PushHook(func(context.Context, int64, string) (*database.AccessToken, error) {
return r0, r1
})
}
func (f *AccessTokensStoreCreateFunc) nextHook() func(context.Context, int64, string) (*database.AccessToken, error) {
f.mutex.Lock()
defer f.mutex.Unlock()
if len(f.hooks) == 0 {
return f.defaultHook
}
hook := f.hooks[0]
f.hooks = f.hooks[1:]
return hook
}
func (f *AccessTokensStoreCreateFunc) appendCall(r0 AccessTokensStoreCreateFuncCall) {
f.mutex.Lock()
f.history = append(f.history, r0)
f.mutex.Unlock()
}
// History returns a sequence of AccessTokensStoreCreateFuncCall objects
// describing the invocations of this function.
func (f *AccessTokensStoreCreateFunc) History() []AccessTokensStoreCreateFuncCall {
f.mutex.Lock()
history := make([]AccessTokensStoreCreateFuncCall, len(f.history))
copy(history, f.history)
f.mutex.Unlock()
return history
}
// AccessTokensStoreCreateFuncCall is an object that describes an invocation
// of method Create on an instance of MockAccessTokensStore.
type AccessTokensStoreCreateFuncCall struct {
// Arg0 is the value of the 1st argument passed to this method
// invocation.
Arg0 context.Context
// Arg1 is the value of the 2nd argument passed to this method
// invocation.
Arg1 int64
// Arg2 is the value of the 3rd argument passed to this method
// invocation.
Arg2 string
// Result0 is the value of the 1st result returned from this method
// invocation.
Result0 *database.AccessToken
// Result1 is the value of the 2nd result returned from this method
// invocation.
Result1 error
}
// Args returns an interface slice containing the arguments of this
// invocation.
func (c AccessTokensStoreCreateFuncCall) Args() []interface{} {
return []interface{}{c.Arg0, c.Arg1, c.Arg2}
}
// Results returns an interface slice containing the results of this
// invocation.
func (c AccessTokensStoreCreateFuncCall) Results() []interface{} {
return []interface{}{c.Result0, c.Result1}
}
// AccessTokensStoreDeleteByIDFunc describes the behavior when the
// DeleteByID method of the parent MockAccessTokensStore instance is
// invoked.
type AccessTokensStoreDeleteByIDFunc struct {
defaultHook func(context.Context, int64, int64) error
hooks []func(context.Context, int64, int64) error
history []AccessTokensStoreDeleteByIDFuncCall
mutex sync.Mutex
}
// DeleteByID delegates to the next hook function in the queue and stores
// the parameter and result values of this invocation.
func (m *MockAccessTokensStore) DeleteByID(v0 context.Context, v1 int64, v2 int64) error {
r0 := m.DeleteByIDFunc.nextHook()(v0, v1, v2)
m.DeleteByIDFunc.appendCall(AccessTokensStoreDeleteByIDFuncCall{v0, v1, v2, r0})
return r0
}
// SetDefaultHook sets function that is called when the DeleteByID method of
// the parent MockAccessTokensStore instance is invoked and the hook queue
// is empty.
func (f *AccessTokensStoreDeleteByIDFunc) SetDefaultHook(hook func(context.Context, int64, int64) error) {
f.defaultHook = hook
}
// PushHook adds a function to the end of hook queue. Each invocation of the
// DeleteByID method of the parent MockAccessTokensStore instance invokes
// the hook at the front of the queue and discards it. After the queue is
// empty, the default hook function is invoked for any future action.
func (f *AccessTokensStoreDeleteByIDFunc) PushHook(hook func(context.Context, int64, int64) error) {
f.mutex.Lock()
f.hooks = append(f.hooks, hook)
f.mutex.Unlock()
}
// SetDefaultReturn calls SetDefaultHook with a function that returns the
// given values.
func (f *AccessTokensStoreDeleteByIDFunc) SetDefaultReturn(r0 error) {
f.SetDefaultHook(func(context.Context, int64, int64) error {
return r0
})
}
// PushReturn calls PushHook with a function that returns the given values.
func (f *AccessTokensStoreDeleteByIDFunc) PushReturn(r0 error) {
f.PushHook(func(context.Context, int64, int64) error {
return r0
})
}
func (f *AccessTokensStoreDeleteByIDFunc) nextHook() func(context.Context, int64, int64) error {
f.mutex.Lock()
defer f.mutex.Unlock()
if len(f.hooks) == 0 {
return f.defaultHook
}
hook := f.hooks[0]
f.hooks = f.hooks[1:]
return hook
}
func (f *AccessTokensStoreDeleteByIDFunc) appendCall(r0 AccessTokensStoreDeleteByIDFuncCall) {
f.mutex.Lock()
f.history = append(f.history, r0)
f.mutex.Unlock()
}
// History returns a sequence of AccessTokensStoreDeleteByIDFuncCall objects
// describing the invocations of this function.
func (f *AccessTokensStoreDeleteByIDFunc) History() []AccessTokensStoreDeleteByIDFuncCall {
f.mutex.Lock()
history := make([]AccessTokensStoreDeleteByIDFuncCall, len(f.history))
copy(history, f.history)
f.mutex.Unlock()
return history
}
// AccessTokensStoreDeleteByIDFuncCall is an object that describes an
// invocation of method DeleteByID on an instance of MockAccessTokensStore.
type AccessTokensStoreDeleteByIDFuncCall struct {
// Arg0 is the value of the 1st argument passed to this method
// invocation.
Arg0 context.Context
// Arg1 is the value of the 2nd argument passed to this method
// invocation.
Arg1 int64
// Arg2 is the value of the 3rd argument passed to this method
// invocation.
Arg2 int64
// Result0 is the value of the 1st result returned from this method
// invocation.
Result0 error
}
// Args returns an interface slice containing the arguments of this
// invocation.
func (c AccessTokensStoreDeleteByIDFuncCall) Args() []interface{} {
return []interface{}{c.Arg0, c.Arg1, c.Arg2}
}
// Results returns an interface slice containing the results of this
// invocation.
func (c AccessTokensStoreDeleteByIDFuncCall) Results() []interface{} {
return []interface{}{c.Result0}
}
// AccessTokensStoreGetBySHA1Func describes the behavior when the GetBySHA1
// method of the parent MockAccessTokensStore instance is invoked.
type AccessTokensStoreGetBySHA1Func struct {
defaultHook func(context.Context, string) (*database.AccessToken, error)
hooks []func(context.Context, string) (*database.AccessToken, error)
history []AccessTokensStoreGetBySHA1FuncCall
mutex sync.Mutex
}
// GetBySHA1 delegates to the next hook function in the queue and stores the
// parameter and result values of this invocation.
func (m *MockAccessTokensStore) GetBySHA1(v0 context.Context, v1 string) (*database.AccessToken, error) {
r0, r1 := m.GetBySHA1Func.nextHook()(v0, v1)
m.GetBySHA1Func.appendCall(AccessTokensStoreGetBySHA1FuncCall{v0, v1, r0, r1})
return r0, r1
}
// SetDefaultHook sets function that is called when the GetBySHA1 method of
// the parent MockAccessTokensStore instance is invoked and the hook queue
// is empty.
func (f *AccessTokensStoreGetBySHA1Func) SetDefaultHook(hook func(context.Context, string) (*database.AccessToken, error)) {
f.defaultHook = hook
}
// PushHook adds a function to the end of hook queue. Each invocation of the
// GetBySHA1 method of the parent MockAccessTokensStore instance invokes the
// hook at the front of the queue and discards it. After the queue is empty,
// the default hook function is invoked for any future action.
func (f *AccessTokensStoreGetBySHA1Func) PushHook(hook func(context.Context, string) (*database.AccessToken, error)) {
f.mutex.Lock()
f.hooks = append(f.hooks, hook)
f.mutex.Unlock()
}
// SetDefaultReturn calls SetDefaultHook with a function that returns the
// given values.
func (f *AccessTokensStoreGetBySHA1Func) SetDefaultReturn(r0 *database.AccessToken, r1 error) {
f.SetDefaultHook(func(context.Context, string) (*database.AccessToken, error) {
return r0, r1
})
}
// PushReturn calls PushHook with a function that returns the given values.
func (f *AccessTokensStoreGetBySHA1Func) PushReturn(r0 *database.AccessToken, r1 error) {
f.PushHook(func(context.Context, string) (*database.AccessToken, error) {
return r0, r1
})
}
func (f *AccessTokensStoreGetBySHA1Func) nextHook() func(context.Context, string) (*database.AccessToken, error) {
f.mutex.Lock()
defer f.mutex.Unlock()
if len(f.hooks) == 0 {
return f.defaultHook
}
hook := f.hooks[0]
f.hooks = f.hooks[1:]
return hook
}
func (f *AccessTokensStoreGetBySHA1Func) appendCall(r0 AccessTokensStoreGetBySHA1FuncCall) {
f.mutex.Lock()
f.history = append(f.history, r0)
f.mutex.Unlock()
}
// History returns a sequence of AccessTokensStoreGetBySHA1FuncCall objects
// describing the invocations of this function.
func (f *AccessTokensStoreGetBySHA1Func) History() []AccessTokensStoreGetBySHA1FuncCall {
f.mutex.Lock()
history := make([]AccessTokensStoreGetBySHA1FuncCall, len(f.history))
copy(history, f.history)
f.mutex.Unlock()
return history
}
// AccessTokensStoreGetBySHA1FuncCall is an object that describes an
// invocation of method GetBySHA1 on an instance of MockAccessTokensStore.
type AccessTokensStoreGetBySHA1FuncCall struct {
// Arg0 is the value of the 1st argument passed to this method
// invocation.
Arg0 context.Context
// Arg1 is the value of the 2nd argument passed to this method
// invocation.
Arg1 string
// Result0 is the value of the 1st result returned from this method
// invocation.
Result0 *database.AccessToken
// Result1 is the value of the 2nd result returned from this method
// invocation.
Result1 error
}
// Args returns an interface slice containing the arguments of this
// invocation.
func (c AccessTokensStoreGetBySHA1FuncCall) Args() []interface{} {
return []interface{}{c.Arg0, c.Arg1}
}
// Results returns an interface slice containing the results of this
// invocation.
func (c AccessTokensStoreGetBySHA1FuncCall) Results() []interface{} {
return []interface{}{c.Result0, c.Result1}
}
// AccessTokensStoreListFunc describes the behavior when the List method of
// the parent MockAccessTokensStore instance is invoked.
type AccessTokensStoreListFunc struct {
defaultHook func(context.Context, int64) ([]*database.AccessToken, error)
hooks []func(context.Context, int64) ([]*database.AccessToken, error)
history []AccessTokensStoreListFuncCall
mutex sync.Mutex
}
// List delegates to the next hook function in the queue and stores the
// parameter and result values of this invocation.
func (m *MockAccessTokensStore) List(v0 context.Context, v1 int64) ([]*database.AccessToken, error) {
r0, r1 := m.ListFunc.nextHook()(v0, v1)
m.ListFunc.appendCall(AccessTokensStoreListFuncCall{v0, v1, r0, r1})
return r0, r1
}
// SetDefaultHook sets function that is called when the List method of the
// parent MockAccessTokensStore instance is invoked and the hook queue is
// empty.
func (f *AccessTokensStoreListFunc) SetDefaultHook(hook func(context.Context, int64) ([]*database.AccessToken, error)) {
f.defaultHook = hook
}
// PushHook adds a function to the end of hook queue. Each invocation of the
// List method of the parent MockAccessTokensStore instance invokes the hook
// at the front of the queue and discards it. After the queue is empty, the
// default hook function is invoked for any future action.
func (f *AccessTokensStoreListFunc) PushHook(hook func(context.Context, int64) ([]*database.AccessToken, error)) {
f.mutex.Lock()
f.hooks = append(f.hooks, hook)
f.mutex.Unlock()
}
// SetDefaultReturn calls SetDefaultHook with a function that returns the
// given values.
func (f *AccessTokensStoreListFunc) SetDefaultReturn(r0 []*database.AccessToken, r1 error) {
f.SetDefaultHook(func(context.Context, int64) ([]*database.AccessToken, error) {
return r0, r1
})
}
// PushReturn calls PushHook with a function that returns the given values.
func (f *AccessTokensStoreListFunc) PushReturn(r0 []*database.AccessToken, r1 error) {
f.PushHook(func(context.Context, int64) ([]*database.AccessToken, error) {
return r0, r1
})
}
func (f *AccessTokensStoreListFunc) nextHook() func(context.Context, int64) ([]*database.AccessToken, error) {
f.mutex.Lock()
defer f.mutex.Unlock()
if len(f.hooks) == 0 {
return f.defaultHook
}
hook := f.hooks[0]
f.hooks = f.hooks[1:]
return hook
}
func (f *AccessTokensStoreListFunc) appendCall(r0 AccessTokensStoreListFuncCall) {
f.mutex.Lock()
f.history = append(f.history, r0)
f.mutex.Unlock()
}
// History returns a sequence of AccessTokensStoreListFuncCall objects
// describing the invocations of this function.
func (f *AccessTokensStoreListFunc) History() []AccessTokensStoreListFuncCall {
f.mutex.Lock()
history := make([]AccessTokensStoreListFuncCall, len(f.history))
copy(history, f.history)
f.mutex.Unlock()
return history
}
// AccessTokensStoreListFuncCall is an object that describes an invocation
// of method List on an instance of MockAccessTokensStore.
type AccessTokensStoreListFuncCall struct {
// Arg0 is the value of the 1st argument passed to this method
// invocation.
Arg0 context.Context
// Arg1 is the value of the 2nd argument passed to this method
// invocation.
Arg1 int64
// Result0 is the value of the 1st result returned from this method
// invocation.
Result0 []*database.AccessToken
// Result1 is the value of the 2nd result returned from this method
// invocation.
Result1 error
}
// Args returns an interface slice containing the arguments of this
// invocation.
func (c AccessTokensStoreListFuncCall) Args() []interface{} {
return []interface{}{c.Arg0, c.Arg1}
}
// Results returns an interface slice containing the results of this
// invocation.
func (c AccessTokensStoreListFuncCall) Results() []interface{} {
return []interface{}{c.Result0, c.Result1}
}
// AccessTokensStoreTouchFunc describes the behavior when the Touch method
// of the parent MockAccessTokensStore instance is invoked.
type AccessTokensStoreTouchFunc struct {
defaultHook func(context.Context, int64) error
hooks []func(context.Context, int64) error
history []AccessTokensStoreTouchFuncCall
mutex sync.Mutex
}
// Touch delegates to the next hook function in the queue and stores the
// parameter and result values of this invocation.
func (m *MockAccessTokensStore) Touch(v0 context.Context, v1 int64) error {
r0 := m.TouchFunc.nextHook()(v0, v1)
m.TouchFunc.appendCall(AccessTokensStoreTouchFuncCall{v0, v1, r0})
return r0
}
// SetDefaultHook sets function that is called when the Touch method of the
// parent MockAccessTokensStore instance is invoked and the hook queue is
// empty.
func (f *AccessTokensStoreTouchFunc) SetDefaultHook(hook func(context.Context, int64) error) {
f.defaultHook = hook
}
// PushHook adds a function to the end of hook queue. Each invocation of the
// Touch method of the parent MockAccessTokensStore instance invokes the
// hook at the front of the queue and discards it. After the queue is empty,
// the default hook function is invoked for any future action.
func (f *AccessTokensStoreTouchFunc) PushHook(hook func(context.Context, int64) error) {
f.mutex.Lock()
f.hooks = append(f.hooks, hook)
f.mutex.Unlock()
}
// SetDefaultReturn calls SetDefaultHook with a function that returns the
// given values.
func (f *AccessTokensStoreTouchFunc) SetDefaultReturn(r0 error) {
f.SetDefaultHook(func(context.Context, int64) error {
return r0
})
}
// PushReturn calls PushHook with a function that returns the given values.
func (f *AccessTokensStoreTouchFunc) PushReturn(r0 error) {
f.PushHook(func(context.Context, int64) error {
return r0
})
}
func (f *AccessTokensStoreTouchFunc) nextHook() func(context.Context, int64) error {
f.mutex.Lock()
defer f.mutex.Unlock()
if len(f.hooks) == 0 {
return f.defaultHook
}
hook := f.hooks[0]
f.hooks = f.hooks[1:]
return hook
}
func (f *AccessTokensStoreTouchFunc) appendCall(r0 AccessTokensStoreTouchFuncCall) {
f.mutex.Lock()
f.history = append(f.history, r0)
f.mutex.Unlock()
}
// History returns a sequence of AccessTokensStoreTouchFuncCall objects
// describing the invocations of this function.
func (f *AccessTokensStoreTouchFunc) History() []AccessTokensStoreTouchFuncCall {
f.mutex.Lock()
history := make([]AccessTokensStoreTouchFuncCall, len(f.history))
copy(history, f.history)
f.mutex.Unlock()
return history
}
// AccessTokensStoreTouchFuncCall is an object that describes an invocation
// of method Touch on an instance of MockAccessTokensStore.
type AccessTokensStoreTouchFuncCall struct {
// Arg0 is the value of the 1st argument passed to this method
// invocation.
Arg0 context.Context
// Arg1 is the value of the 2nd argument passed to this method
// invocation.
Arg1 int64
// Result0 is the value of the 1st result returned from this method
// invocation.
Result0 error
}
// Args returns an interface slice containing the arguments of this
// invocation.
func (c AccessTokensStoreTouchFuncCall) Args() []interface{} {
return []interface{}{c.Arg0, c.Arg1}
}
// Results returns an interface slice containing the results of this
// invocation.
func (c AccessTokensStoreTouchFuncCall) Results() []interface{} {
return []interface{}{c.Result0}
}
// MockLFSStore is a mock implementation of the LFSStore interface (from the // MockLFSStore is a mock implementation of the LFSStore interface (from the
// package gogs.io/gogs/internal/database) used for unit testing. // package gogs.io/gogs/internal/database) used for unit testing.
type MockLFSStore struct { type MockLFSStore struct {
@ -6698,3 +6047,274 @@ func (c UsersStoreUseCustomAvatarFuncCall) Args() []interface{} {
func (c UsersStoreUseCustomAvatarFuncCall) Results() []interface{} { func (c UsersStoreUseCustomAvatarFuncCall) Results() []interface{} {
return []interface{}{c.Result0} return []interface{}{c.Result0}
} }
// MockStore is a mock implementation of the Store interface (from the
// package gogs.io/gogs/internal/route/lfs) used for unit testing.
type MockStore struct {
// GetAccessTokenBySHA1Func is an instance of a mock function object
// controlling the behavior of the method GetAccessTokenBySHA1.
GetAccessTokenBySHA1Func *StoreGetAccessTokenBySHA1Func
// TouchAccessTokenByIDFunc is an instance of a mock function object
// controlling the behavior of the method TouchAccessTokenByID.
TouchAccessTokenByIDFunc *StoreTouchAccessTokenByIDFunc
}
// NewMockStore creates a new mock of the Store interface. All methods
// return zero values for all results, unless overwritten.
func NewMockStore() *MockStore {
return &MockStore{
GetAccessTokenBySHA1Func: &StoreGetAccessTokenBySHA1Func{
defaultHook: func(context.Context, string) (r0 *database.AccessToken, r1 error) {
return
},
},
TouchAccessTokenByIDFunc: &StoreTouchAccessTokenByIDFunc{
defaultHook: func(context.Context, int64) (r0 error) {
return
},
},
}
}
// NewStrictMockStore creates a new mock of the Store interface. All methods
// panic on invocation, unless overwritten.
func NewStrictMockStore() *MockStore {
return &MockStore{
GetAccessTokenBySHA1Func: &StoreGetAccessTokenBySHA1Func{
defaultHook: func(context.Context, string) (*database.AccessToken, error) {
panic("unexpected invocation of MockStore.GetAccessTokenBySHA1")
},
},
TouchAccessTokenByIDFunc: &StoreTouchAccessTokenByIDFunc{
defaultHook: func(context.Context, int64) error {
panic("unexpected invocation of MockStore.TouchAccessTokenByID")
},
},
}
}
// NewMockStoreFrom creates a new mock of the MockStore interface. All
// methods delegate to the given implementation, unless overwritten.
func NewMockStoreFrom(i Store) *MockStore {
return &MockStore{
GetAccessTokenBySHA1Func: &StoreGetAccessTokenBySHA1Func{
defaultHook: i.GetAccessTokenBySHA1,
},
TouchAccessTokenByIDFunc: &StoreTouchAccessTokenByIDFunc{
defaultHook: i.TouchAccessTokenByID,
},
}
}
// StoreGetAccessTokenBySHA1Func describes the behavior when the
// GetAccessTokenBySHA1 method of the parent MockStore instance is invoked.
type StoreGetAccessTokenBySHA1Func struct {
defaultHook func(context.Context, string) (*database.AccessToken, error)
hooks []func(context.Context, string) (*database.AccessToken, error)
history []StoreGetAccessTokenBySHA1FuncCall
mutex sync.Mutex
}
// GetAccessTokenBySHA1 delegates to the next hook function in the queue and
// stores the parameter and result values of this invocation.
func (m *MockStore) GetAccessTokenBySHA1(v0 context.Context, v1 string) (*database.AccessToken, error) {
r0, r1 := m.GetAccessTokenBySHA1Func.nextHook()(v0, v1)
m.GetAccessTokenBySHA1Func.appendCall(StoreGetAccessTokenBySHA1FuncCall{v0, v1, r0, r1})
return r0, r1
}
// SetDefaultHook sets function that is called when the GetAccessTokenBySHA1
// method of the parent MockStore instance is invoked and the hook queue is
// empty.
func (f *StoreGetAccessTokenBySHA1Func) SetDefaultHook(hook func(context.Context, string) (*database.AccessToken, error)) {
f.defaultHook = hook
}
// PushHook adds a function to the end of hook queue. Each invocation of the
// GetAccessTokenBySHA1 method of the parent MockStore instance invokes the
// hook at the front of the queue and discards it. After the queue is empty,
// the default hook function is invoked for any future action.
func (f *StoreGetAccessTokenBySHA1Func) PushHook(hook func(context.Context, string) (*database.AccessToken, error)) {
f.mutex.Lock()
f.hooks = append(f.hooks, hook)
f.mutex.Unlock()
}
// SetDefaultReturn calls SetDefaultHook with a function that returns the
// given values.
func (f *StoreGetAccessTokenBySHA1Func) SetDefaultReturn(r0 *database.AccessToken, r1 error) {
f.SetDefaultHook(func(context.Context, string) (*database.AccessToken, error) {
return r0, r1
})
}
// PushReturn calls PushHook with a function that returns the given values.
func (f *StoreGetAccessTokenBySHA1Func) PushReturn(r0 *database.AccessToken, r1 error) {
f.PushHook(func(context.Context, string) (*database.AccessToken, error) {
return r0, r1
})
}
func (f *StoreGetAccessTokenBySHA1Func) nextHook() func(context.Context, string) (*database.AccessToken, error) {
f.mutex.Lock()
defer f.mutex.Unlock()
if len(f.hooks) == 0 {
return f.defaultHook
}
hook := f.hooks[0]
f.hooks = f.hooks[1:]
return hook
}
func (f *StoreGetAccessTokenBySHA1Func) appendCall(r0 StoreGetAccessTokenBySHA1FuncCall) {
f.mutex.Lock()
f.history = append(f.history, r0)
f.mutex.Unlock()
}
// History returns a sequence of StoreGetAccessTokenBySHA1FuncCall objects
// describing the invocations of this function.
func (f *StoreGetAccessTokenBySHA1Func) History() []StoreGetAccessTokenBySHA1FuncCall {
f.mutex.Lock()
history := make([]StoreGetAccessTokenBySHA1FuncCall, len(f.history))
copy(history, f.history)
f.mutex.Unlock()
return history
}
// StoreGetAccessTokenBySHA1FuncCall is an object that describes an
// invocation of method GetAccessTokenBySHA1 on an instance of MockStore.
type StoreGetAccessTokenBySHA1FuncCall struct {
// Arg0 is the value of the 1st argument passed to this method
// invocation.
Arg0 context.Context
// Arg1 is the value of the 2nd argument passed to this method
// invocation.
Arg1 string
// Result0 is the value of the 1st result returned from this method
// invocation.
Result0 *database.AccessToken
// Result1 is the value of the 2nd result returned from this method
// invocation.
Result1 error
}
// Args returns an interface slice containing the arguments of this
// invocation.
func (c StoreGetAccessTokenBySHA1FuncCall) Args() []interface{} {
return []interface{}{c.Arg0, c.Arg1}
}
// Results returns an interface slice containing the results of this
// invocation.
func (c StoreGetAccessTokenBySHA1FuncCall) Results() []interface{} {
return []interface{}{c.Result0, c.Result1}
}
// StoreTouchAccessTokenByIDFunc describes the behavior when the
// TouchAccessTokenByID method of the parent MockStore instance is invoked.
type StoreTouchAccessTokenByIDFunc struct {
defaultHook func(context.Context, int64) error
hooks []func(context.Context, int64) error
history []StoreTouchAccessTokenByIDFuncCall
mutex sync.Mutex
}
// TouchAccessTokenByID delegates to the next hook function in the queue and
// stores the parameter and result values of this invocation.
func (m *MockStore) TouchAccessTokenByID(v0 context.Context, v1 int64) error {
r0 := m.TouchAccessTokenByIDFunc.nextHook()(v0, v1)
m.TouchAccessTokenByIDFunc.appendCall(StoreTouchAccessTokenByIDFuncCall{v0, v1, r0})
return r0
}
// SetDefaultHook sets function that is called when the TouchAccessTokenByID
// method of the parent MockStore instance is invoked and the hook queue is
// empty.
func (f *StoreTouchAccessTokenByIDFunc) SetDefaultHook(hook func(context.Context, int64) error) {
f.defaultHook = hook
}
// PushHook adds a function to the end of hook queue. Each invocation of the
// TouchAccessTokenByID method of the parent MockStore instance invokes the
// hook at the front of the queue and discards it. After the queue is empty,
// the default hook function is invoked for any future action.
func (f *StoreTouchAccessTokenByIDFunc) PushHook(hook func(context.Context, int64) error) {
f.mutex.Lock()
f.hooks = append(f.hooks, hook)
f.mutex.Unlock()
}
// SetDefaultReturn calls SetDefaultHook with a function that returns the
// given values.
func (f *StoreTouchAccessTokenByIDFunc) SetDefaultReturn(r0 error) {
f.SetDefaultHook(func(context.Context, int64) error {
return r0
})
}
// PushReturn calls PushHook with a function that returns the given values.
func (f *StoreTouchAccessTokenByIDFunc) PushReturn(r0 error) {
f.PushHook(func(context.Context, int64) error {
return r0
})
}
func (f *StoreTouchAccessTokenByIDFunc) nextHook() func(context.Context, int64) error {
f.mutex.Lock()
defer f.mutex.Unlock()
if len(f.hooks) == 0 {
return f.defaultHook
}
hook := f.hooks[0]
f.hooks = f.hooks[1:]
return hook
}
func (f *StoreTouchAccessTokenByIDFunc) appendCall(r0 StoreTouchAccessTokenByIDFuncCall) {
f.mutex.Lock()
f.history = append(f.history, r0)
f.mutex.Unlock()
}
// History returns a sequence of StoreTouchAccessTokenByIDFuncCall objects
// describing the invocations of this function.
func (f *StoreTouchAccessTokenByIDFunc) History() []StoreTouchAccessTokenByIDFuncCall {
f.mutex.Lock()
history := make([]StoreTouchAccessTokenByIDFuncCall, len(f.history))
copy(history, f.history)
f.mutex.Unlock()
return history
}
// StoreTouchAccessTokenByIDFuncCall is an object that describes an
// invocation of method TouchAccessTokenByID on an instance of MockStore.
type StoreTouchAccessTokenByIDFuncCall struct {
// Arg0 is the value of the 1st argument passed to this method
// invocation.
Arg0 context.Context
// Arg1 is the value of the 2nd argument passed to this method
// invocation.
Arg1 int64
// Result0 is the value of the 1st result returned from this method
// invocation.
Result0 error
}
// Args returns an interface slice containing the arguments of this
// invocation.
func (c StoreTouchAccessTokenByIDFuncCall) Args() []interface{} {
return []interface{}{c.Arg0, c.Arg1}
}
// Results returns an interface slice containing the results of this
// invocation.
func (c StoreTouchAccessTokenByIDFuncCall) Results() []interface{} {
return []interface{}{c.Result0}
}

View File

@ -19,12 +19,14 @@ import (
"gogs.io/gogs/internal/lfsutil" "gogs.io/gogs/internal/lfsutil"
) )
// RegisterRoutes registers LFS routes using given router, and inherits all groups and middleware. // RegisterRoutes registers LFS routes using given router, and inherits all
// groups and middleware.
func RegisterRoutes(r *macaron.Router) { func RegisterRoutes(r *macaron.Router) {
verifyAccept := verifyHeader("Accept", contentType, http.StatusNotAcceptable) verifyAccept := verifyHeader("Accept", contentType, http.StatusNotAcceptable)
verifyContentTypeJSON := verifyHeader("Content-Type", contentType, http.StatusBadRequest) verifyContentTypeJSON := verifyHeader("Content-Type", contentType, http.StatusBadRequest)
verifyContentTypeStream := verifyHeader("Content-Type", "application/octet-stream", http.StatusBadRequest) verifyContentTypeStream := verifyHeader("Content-Type", "application/octet-stream", http.StatusBadRequest)
store := NewStore()
r.Group("", func() { r.Group("", func() {
r.Post("/objects/batch", authorize(database.AccessModeRead), verifyAccept, verifyContentTypeJSON, serveBatch) r.Post("/objects/batch", authorize(database.AccessModeRead), verifyAccept, verifyContentTypeJSON, serveBatch)
r.Group("/objects/basic", func() { r.Group("/objects/basic", func() {
@ -39,12 +41,12 @@ func RegisterRoutes(r *macaron.Router) {
Put(authorize(database.AccessModeWrite), verifyContentTypeStream, basic.serveUpload) Put(authorize(database.AccessModeWrite), verifyContentTypeStream, basic.serveUpload)
r.Post("/verify", authorize(database.AccessModeWrite), verifyAccept, verifyContentTypeJSON, basic.serveVerify) r.Post("/verify", authorize(database.AccessModeWrite), verifyAccept, verifyContentTypeJSON, basic.serveVerify)
}) })
}, authenticate()) }, authenticate(store))
} }
// authenticate tries to authenticate user via HTTP Basic Auth. It first tries to authenticate // authenticate tries to authenticate user via HTTP Basic Auth. It first tries to authenticate
// as plain username and password, then use username as access token if previous step failed. // as plain username and password, then use username as access token if previous step failed.
func authenticate() macaron.Handler { func authenticate(store Store) macaron.Handler {
askCredentials := func(w http.ResponseWriter) { askCredentials := func(w http.ResponseWriter) {
w.Header().Set("Lfs-Authenticate", `Basic realm="Git LFS"`) w.Header().Set("Lfs-Authenticate", `Basic realm="Git LFS"`)
responseJSON(w, http.StatusUnauthorized, responseError{ responseJSON(w, http.StatusUnauthorized, responseError{
@ -74,14 +76,14 @@ func authenticate() macaron.Handler {
// If username and password combination failed, try again using either username // If username and password combination failed, try again using either username
// or password as the token. // or password as the token.
if auth.IsErrBadCredentials(err) { if auth.IsErrBadCredentials(err) {
user, err = context.AuthenticateByToken(c.Req.Context(), username) user, err = context.AuthenticateByToken(store, c.Req.Context(), username)
if err != nil && !database.IsErrAccessTokenNotExist(err) { if err != nil && !database.IsErrAccessTokenNotExist(err) {
internalServerError(c.Resp) internalServerError(c.Resp)
log.Error("Failed to authenticate by access token via username: %v", err) log.Error("Failed to authenticate by access token via username: %v", err)
return return
} else if database.IsErrAccessTokenNotExist(err) { } else if database.IsErrAccessTokenNotExist(err) {
// Try again using the password field as the token. // Try again using the password field as the token.
user, err = context.AuthenticateByToken(c.Req.Context(), password) user, err = context.AuthenticateByToken(store, c.Req.Context(), password)
if err != nil { if err != nil {
if database.IsErrAccessTokenNotExist(err) { if database.IsErrAccessTokenNotExist(err) {
askCredentials(c.Resp) askCredentials(c.Resp)

View File

@ -20,22 +20,16 @@ import (
"gogs.io/gogs/internal/lfsutil" "gogs.io/gogs/internal/lfsutil"
) )
func Test_authenticate(t *testing.T) { func TestAuthenticate(t *testing.T) {
m := macaron.New()
m.Use(macaron.Renderer())
m.Get("/", authenticate(), func(w http.ResponseWriter, user *database.User) {
_, _ = fmt.Fprintf(w, "ID: %d, Name: %s", user.ID, user.Name)
})
tests := []struct { tests := []struct {
name string name string
header http.Header header http.Header
mockUsersStore func() database.UsersStore mockUsersStore func() database.UsersStore
mockTwoFactorsStore func() database.TwoFactorsStore mockTwoFactorsStore func() database.TwoFactorsStore
mockAccessTokensStore func() database.AccessTokensStore mockStore func() *MockStore
expStatusCode int expStatusCode int
expHeader http.Header expHeader http.Header
expBody string expBody string
}{ }{
{ {
name: "no authorization", name: "no authorization",
@ -75,10 +69,10 @@ func Test_authenticate(t *testing.T) {
mock.AuthenticateFunc.SetDefaultReturn(nil, auth.ErrBadCredentials{}) mock.AuthenticateFunc.SetDefaultReturn(nil, auth.ErrBadCredentials{})
return mock return mock
}, },
mockAccessTokensStore: func() database.AccessTokensStore { mockStore: func() *MockStore {
mock := NewMockAccessTokensStore() mockStore := NewMockStore()
mock.GetBySHA1Func.SetDefaultReturn(nil, database.ErrAccessTokenNotExist{}) mockStore.GetAccessTokenBySHA1Func.SetDefaultReturn(nil, database.ErrAccessTokenNotExist{})
return mock return mockStore
}, },
expStatusCode: http.StatusUnauthorized, expStatusCode: http.StatusUnauthorized,
expHeader: http.Header{ expHeader: http.Header{
@ -118,10 +112,10 @@ func Test_authenticate(t *testing.T) {
mock.GetByIDFunc.SetDefaultReturn(&database.User{ID: 1, Name: "unknwon"}, nil) mock.GetByIDFunc.SetDefaultReturn(&database.User{ID: 1, Name: "unknwon"}, nil)
return mock return mock
}, },
mockAccessTokensStore: func() database.AccessTokensStore { mockStore: func() *MockStore {
mock := NewMockAccessTokensStore() mockStore := NewMockStore()
mock.GetBySHA1Func.SetDefaultReturn(&database.AccessToken{}, nil) mockStore.GetAccessTokenBySHA1Func.SetDefaultReturn(&database.AccessToken{}, nil)
return mock return mockStore
}, },
expStatusCode: http.StatusOK, expStatusCode: http.StatusOK,
expHeader: http.Header{}, expHeader: http.Header{},
@ -138,15 +132,15 @@ func Test_authenticate(t *testing.T) {
mock.GetByIDFunc.SetDefaultReturn(&database.User{ID: 1, Name: "unknwon"}, nil) mock.GetByIDFunc.SetDefaultReturn(&database.User{ID: 1, Name: "unknwon"}, nil)
return mock return mock
}, },
mockAccessTokensStore: func() database.AccessTokensStore { mockStore: func() *MockStore {
mock := NewMockAccessTokensStore() mockStore := NewMockStore()
mock.GetBySHA1Func.SetDefaultHook(func(ctx context.Context, sha1 string) (*database.AccessToken, error) { mockStore.GetAccessTokenBySHA1Func.SetDefaultHook(func(_ context.Context, sha1 string) (*database.AccessToken, error) {
if sha1 == "password" { if sha1 == "password" {
return &database.AccessToken{}, nil return &database.AccessToken{}, nil
} }
return nil, database.ErrAccessTokenNotExist{} return nil, database.ErrAccessTokenNotExist{}
}) })
return mock return mockStore
}, },
expStatusCode: http.StatusOK, expStatusCode: http.StatusOK,
expHeader: http.Header{}, expHeader: http.Header{},
@ -161,10 +155,16 @@ func Test_authenticate(t *testing.T) {
if test.mockTwoFactorsStore != nil { if test.mockTwoFactorsStore != nil {
database.SetMockTwoFactorsStore(t, test.mockTwoFactorsStore()) database.SetMockTwoFactorsStore(t, test.mockTwoFactorsStore())
} }
if test.mockAccessTokensStore != nil { if test.mockStore == nil {
database.SetMockAccessTokensStore(t, test.mockAccessTokensStore()) test.mockStore = NewMockStore
} }
m := macaron.New()
m.Use(macaron.Renderer())
m.Get("/", authenticate(test.mockStore()), func(w http.ResponseWriter, user *database.User) {
_, _ = fmt.Fprintf(w, "ID: %d, Name: %s", user.ID, user.Name)
})
r, err := http.NewRequest("GET", "/", nil) r, err := http.NewRequest("GET", "/", nil)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)

View File

@ -0,0 +1,34 @@
package lfs
import (
"context"
"gogs.io/gogs/internal/database"
)
// Store is the data layer carrier for LFS endpoints. This interface is meant to
// abstract away and limit the exposure of the underlying data layer to the
// handler through a thin-wrapper.
type Store interface {
// GetAccessTokenBySHA1 returns the access token with given SHA1. It returns
// database.ErrAccessTokenNotExist when not found.
GetAccessTokenBySHA1(ctx context.Context, sha1 string) (*database.AccessToken, error)
// TouchAccessTokenByID updates the updated time of the given access token to
// the current time.
TouchAccessTokenByID(ctx context.Context, id int64) error
}
type store struct{}
// NewStore returns a new Store using the global database handle.
func NewStore() Store {
return &store{}
}
func (*store) GetAccessTokenBySHA1(ctx context.Context, sha1 string) (*database.AccessToken, error) {
return database.Handle.AccessTokens().GetBySHA1(ctx, sha1)
}
func (*store) TouchAccessTokenByID(ctx context.Context, id int64) error {
return database.Handle.AccessTokens().Touch(ctx, id)
}

View File

@ -44,7 +44,7 @@ func askCredentials(c *macaron.Context, status int, text string) {
c.Error(status, text) c.Error(status, text)
} }
func HTTPContexter() macaron.Handler { func HTTPContexter(store Store) macaron.Handler {
return func(c *macaron.Context) { return func(c *macaron.Context) {
if len(conf.HTTP.AccessControlAllowOrigin) > 0 { if len(conf.HTTP.AccessControlAllowOrigin) > 0 {
// Set CORS headers for browser-based git clients // Set CORS headers for browser-based git clients
@ -134,14 +134,14 @@ func HTTPContexter() macaron.Handler {
// If username and password combination failed, try again using either username // If username and password combination failed, try again using either username
// or password as the token. // or password as the token.
if authUser == nil { if authUser == nil {
authUser, err = context.AuthenticateByToken(c.Req.Context(), authUsername) authUser, err = context.AuthenticateByToken(store, c.Req.Context(), authUsername)
if err != nil && !database.IsErrAccessTokenNotExist(err) { if err != nil && !database.IsErrAccessTokenNotExist(err) {
c.Status(http.StatusInternalServerError) c.Status(http.StatusInternalServerError)
log.Error("Failed to authenticate by access token via username: %v", err) log.Error("Failed to authenticate by access token via username: %v", err)
return return
} else if database.IsErrAccessTokenNotExist(err) { } else if database.IsErrAccessTokenNotExist(err) {
// Try again using the password field as the token. // Try again using the password field as the token.
authUser, err = context.AuthenticateByToken(c.Req.Context(), authPassword) authUser, err = context.AuthenticateByToken(store, c.Req.Context(), authPassword)
if err != nil { if err != nil {
if database.IsErrAccessTokenNotExist(err) { if database.IsErrAccessTokenNotExist(err) {
askCredentials(c, http.StatusUnauthorized, "") askCredentials(c, http.StatusUnauthorized, "")

View File

@ -0,0 +1,34 @@
package repo
import (
"context"
"gogs.io/gogs/internal/database"
)
// Store is the data layer carrier for context middleware. This interface is
// meant to abstract away and limit the exposure of the underlying data layer to
// the handler through a thin-wrapper.
type Store interface {
// GetAccessTokenBySHA1 returns the access token with given SHA1. It returns
// database.ErrAccessTokenNotExist when not found.
GetAccessTokenBySHA1(ctx context.Context, sha1 string) (*database.AccessToken, error)
// TouchAccessTokenByID updates the updated time of the given access token to
// the current time.
TouchAccessTokenByID(ctx context.Context, id int64) error
}
type store struct{}
// NewStore returns a new Store using the global database handle.
func NewStore() Store {
return &store{}
}
func (*store) GetAccessTokenBySHA1(ctx context.Context, sha1 string) (*database.AccessToken, error) {
return database.Handle.AccessTokens().GetBySHA1(ctx, sha1)
}
func (*store) TouchAccessTokenByID(ctx context.Context, id int64) error {
return database.Handle.AccessTokens().Touch(ctx, id)
}

View File

@ -6,6 +6,7 @@ package user
import ( import (
"bytes" "bytes"
gocontext "context"
"encoding/base64" "encoding/base64"
"fmt" "fmt"
"html/template" "html/template"
@ -15,6 +16,7 @@ import (
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/pquerna/otp" "github.com/pquerna/otp"
"github.com/pquerna/otp/totp" "github.com/pquerna/otp/totp"
"gopkg.in/macaron.v1"
log "unknwon.dev/clog/v2" log "unknwon.dev/clog/v2"
"gogs.io/gogs/internal/auth" "gogs.io/gogs/internal/auth"
@ -28,6 +30,18 @@ import (
"gogs.io/gogs/internal/userutil" "gogs.io/gogs/internal/userutil"
) )
// SettingsHandler is the handler for users settings endpoints.
type SettingsHandler struct {
store SettingsStore
}
// NewSettingsHandler returns a new SettingsHandler for users settings endpoints.
func NewSettingsHandler(s SettingsStore) *SettingsHandler {
return &SettingsHandler{
store: s,
}
}
const ( const (
SETTINGS_PROFILE = "user/settings/profile" SETTINGS_PROFILE = "user/settings/profile"
SETTINGS_AVATAR = "user/settings/avatar" SETTINGS_AVATAR = "user/settings/avatar"
@ -580,62 +594,68 @@ func SettingsLeaveOrganization(c *context.Context) {
}) })
} }
func SettingsApplications(c *context.Context) { func (h *SettingsHandler) Applications() macaron.Handler {
c.Title("settings.applications") return func(c *context.Context) {
c.PageIs("SettingsApplications") c.Title("settings.applications")
c.PageIs("SettingsApplications")
tokens, err := database.AccessTokens.List(c.Req.Context(), c.User.ID) tokens, err := h.store.ListAccessTokens(c.Req.Context(), c.User.ID)
if err != nil {
c.Errorf(err, "list access tokens")
return
}
c.Data["Tokens"] = tokens
c.Success(SETTINGS_APPLICATIONS)
}
func SettingsApplicationsPost(c *context.Context, f form.NewAccessToken) {
c.Title("settings.applications")
c.PageIs("SettingsApplications")
if c.HasError() {
tokens, err := database.AccessTokens.List(c.Req.Context(), c.User.ID)
if err != nil { if err != nil {
c.Errorf(err, "list access tokens") c.Errorf(err, "list access tokens")
return return
} }
c.Data["Tokens"] = tokens c.Data["Tokens"] = tokens
c.Success(SETTINGS_APPLICATIONS) c.Success(SETTINGS_APPLICATIONS)
return
} }
t, err := database.AccessTokens.Create(c.Req.Context(), c.User.ID, f.Name)
if err != nil {
if database.IsErrAccessTokenAlreadyExist(err) {
c.Flash.Error(c.Tr("settings.token_name_exists"))
c.RedirectSubpath("/user/settings/applications")
} else {
c.Errorf(err, "new access token")
}
return
}
c.Flash.Success(c.Tr("settings.generate_token_succees"))
c.Flash.Info(t.Sha1)
c.RedirectSubpath("/user/settings/applications")
} }
func SettingsDeleteApplication(c *context.Context) { func (h *SettingsHandler) ApplicationsPost() macaron.Handler {
if err := database.AccessTokens.DeleteByID(c.Req.Context(), c.User.ID, c.QueryInt64("id")); err != nil { return func(c *context.Context, f form.NewAccessToken) {
c.Flash.Error("DeleteAccessTokenByID: " + err.Error()) c.Title("settings.applications")
} else { c.PageIs("SettingsApplications")
c.Flash.Success(c.Tr("settings.delete_token_success"))
}
c.JSONSuccess(map[string]any{ if c.HasError() {
"redirect": conf.Server.Subpath + "/user/settings/applications", tokens, err := h.store.ListAccessTokens(c.Req.Context(), c.User.ID)
}) if err != nil {
c.Errorf(err, "list access tokens")
return
}
c.Data["Tokens"] = tokens
c.Success(SETTINGS_APPLICATIONS)
return
}
t, err := h.store.CreateAccessToken(c.Req.Context(), c.User.ID, f.Name)
if err != nil {
if database.IsErrAccessTokenAlreadyExist(err) {
c.Flash.Error(c.Tr("settings.token_name_exists"))
c.RedirectSubpath("/user/settings/applications")
} else {
c.Errorf(err, "new access token")
}
return
}
c.Flash.Success(c.Tr("settings.generate_token_succees"))
c.Flash.Info(t.Sha1)
c.RedirectSubpath("/user/settings/applications")
}
}
func (h *SettingsHandler) DeleteApplication() macaron.Handler {
return func(c *context.Context) {
if err := h.store.DeleteAccessTokenByID(c.Req.Context(), c.User.ID, c.QueryInt64("id")); err != nil {
c.Flash.Error("DeleteAccessTokenByID: " + err.Error())
} else {
c.Flash.Success(c.Tr("settings.delete_token_success"))
}
c.JSONSuccess(map[string]any{
"redirect": conf.Server.Subpath + "/user/settings/applications",
})
}
} }
func SettingsDelete(c *context.Context) { func SettingsDelete(c *context.Context) {
@ -672,3 +692,51 @@ func SettingsDelete(c *context.Context) {
c.Success(SETTINGS_DELETE) c.Success(SETTINGS_DELETE)
} }
// SettingsStore is the data layer carrier for user settings endpoints. This
// interface is meant to abstract away and limit the exposure of the underlying
// data layer to the handler through a thin-wrapper.
type SettingsStore interface {
// CreateAccessToken creates a new access token and persist to database. It
// returns database.ErrAccessTokenAlreadyExist when an access token with same
// name already exists for the user.
CreateAccessToken(ctx gocontext.Context, userID int64, name string) (*database.AccessToken, error)
// GetAccessTokenBySHA1 returns the access token with given SHA1. It returns
// database.ErrAccessTokenNotExist when not found.
GetAccessTokenBySHA1(ctx gocontext.Context, sha1 string) (*database.AccessToken, error)
// TouchAccessTokenByID updates the updated time of the given access token to
// the current time.
TouchAccessTokenByID(ctx gocontext.Context, id int64) error
// ListAccessTokens returns all access tokens belongs to given user.
ListAccessTokens(ctx gocontext.Context, userID int64) ([]*database.AccessToken, error)
// DeleteAccessTokenByID deletes the access token by given ID.
DeleteAccessTokenByID(ctx gocontext.Context, userID, id int64) error
}
type settingsStore struct{}
// NewSettingsStore returns a new SettingsStore using the global database
// handle.
func NewSettingsStore() SettingsStore {
return &settingsStore{}
}
func (*settingsStore) CreateAccessToken(ctx gocontext.Context, userID int64, name string) (*database.AccessToken, error) {
return database.Handle.AccessTokens().Create(ctx, userID, name)
}
func (*settingsStore) GetAccessTokenBySHA1(ctx gocontext.Context, sha1 string) (*database.AccessToken, error) {
return database.Handle.AccessTokens().GetBySHA1(ctx, sha1)
}
func (*settingsStore) TouchAccessTokenByID(ctx gocontext.Context, id int64) error {
return database.Handle.AccessTokens().Touch(ctx, id)
}
func (*settingsStore) ListAccessTokens(ctx gocontext.Context, userID int64) ([]*database.AccessToken, error) {
return database.Handle.AccessTokens().List(ctx, userID)
}
func (*settingsStore) DeleteAccessTokenByID(ctx gocontext.Context, userID, id int64) error {
return database.Handle.AccessTokens().DeleteByID(ctx, userID, id)
}

View File

@ -39,6 +39,8 @@ mocks:
- LFSStore - LFSStore
- UsersStore - UsersStore
- TwoFactorsStore - TwoFactorsStore
- AccessTokensStore
- ReposStore - ReposStore
- PermsStore - PermsStore
- path: gogs.io/gogs/internal/route/lfs
interfaces:
- Store