From 8054ffc12f22253ec75ddd5e18cd5878a7fd990f Mon Sep 17 00:00:00 2001 From: Joe Chen Date: Tue, 20 Feb 2024 21:47:32 -0500 Subject: [PATCH] all: unwrap `database.AccessTokensStore` interface (#7670) --- .github/workflows/go.yml | 10 +- internal/cmd/web.go | 12 +- internal/context/auth.go | 25 +- internal/context/context.go | 4 +- internal/context/store.go | 34 + internal/database/access_tokens.go | 81 +- internal/database/access_tokens_test.go | 62 +- internal/database/database.go | 30 +- internal/database/mocks.go | 8 - internal/database/models.go | 12 +- internal/route/api/v1/api.go | 5 +- internal/route/api/v1/user/access_tokens.go | 88 ++ internal/route/api/v1/user/app.go | 41 - internal/route/install.go | 4 +- internal/route/lfs/mocks_test.go | 922 ++++++-------------- internal/route/lfs/route.go | 12 +- internal/route/lfs/route_test.go | 58 +- internal/route/lfs/store.go | 34 + internal/route/repo/http.go | 6 +- internal/route/repo/store.go | 34 + internal/route/user/setting.go | 158 +++- mockgen.yaml | 4 +- 22 files changed, 752 insertions(+), 892 deletions(-) create mode 100644 internal/context/store.go create mode 100644 internal/route/api/v1/user/access_tokens.go delete mode 100644 internal/route/api/v1/user/app.go create mode 100644 internal/route/lfs/store.go create mode 100644 internal/route/repo/store.go diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index 52b2cf4e6..4287444ef 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -62,7 +62,7 @@ jobs: name: Test strategy: matrix: - go-version: [ 1.21.x, 1.22.x ] + go-version: [ 1.22.x ] platform: [ ubuntu-latest, macos-latest ] runs-on: ${{ matrix.platform }} steps: @@ -102,7 +102,7 @@ jobs: name: Test Windows strategy: matrix: - go-version: [ 1.21.x, 1.22.x ] + go-version: [ 1.22.x ] platform: [ windows-latest ] runs-on: ${{ matrix.platform }} steps: @@ -140,7 +140,7 @@ jobs: name: Postgres strategy: matrix: - go-version: [ 1.21.x, 1.22.x ] + go-version: [ 1.22.x ] platform: [ ubuntu-latest ] runs-on: ${{ matrix.platform }} services: @@ -176,7 +176,7 @@ jobs: name: MySQL strategy: matrix: - go-version: [ 1.21.x, 1.22.x ] + go-version: [ 1.22.x ] platform: [ ubuntu-20.04 ] runs-on: ${{ matrix.platform }} steps: @@ -201,7 +201,7 @@ jobs: name: SQLite - Go strategy: matrix: - go-version: [ 1.21.x, 1.22.x ] + go-version: [ 1.22.x ] platform: [ ubuntu-latest ] runs-on: ${{ matrix.platform }} steps: diff --git a/internal/cmd/web.go b/internal/cmd/web.go index 54b02ff95..70cbeb45f 100644 --- a/internal/cmd/web.go +++ b/internal/cmd/web.go @@ -237,9 +237,11 @@ func runWeb(c *cli.Context) error { m.Get("", user.SettingsOrganizations) m.Post("/leave", user.SettingsLeaveOrganization) }) - m.Combo("/applications").Get(user.SettingsApplications). - Post(bindIgnErr(form.NewAccessToken{}), user.SettingsApplicationsPost) - m.Post("/applications/delete", user.SettingsDeleteApplication) + + settingsHandler := user.NewSettingsHandler(user.NewSettingsStore()) + 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) }, reqSignIn, func(c *context.Context) { c.Data["PageIsUserSettings"] = true @@ -652,7 +654,7 @@ func runWeb(c *cli.Context) error { SetCookie: true, 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) }) - m.Route("/*", "GET,POST,OPTIONS", context.ServeGoGet(), repo.HTTPContexter(), repo.HTTP) + m.Route("/*", "GET,POST,OPTIONS", context.ServeGoGet(), repo.HTTPContexter(repo.NewStore()), repo.HTTP) }) // *************************** diff --git a/internal/context/auth.go b/internal/context/auth.go index a1b489a8b..f98f27971 100644 --- a/internal/context/auth.go +++ b/internal/context/auth.go @@ -106,9 +106,18 @@ func isAPIPath(url string) bool { 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 // 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 { return 0, false } @@ -132,14 +141,14 @@ func authenticatedUserID(c *macaron.Context, sess session.Store) (_ int64, isTok // Let's see if token is valid. 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 !database.IsErrAccessTokenNotExist(err) { log.Error("GetAccessTokenBySHA: %v", err) } 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) } 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 // 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 { return nil, false, false } - uid, isTokenAuth := authenticatedUserID(ctx, sess) + uid, isTokenAuth := authenticatedUserID(store, ctx, sess) if uid <= 0 { 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 // token. It returns database.ErrAccessTokenNotExist when the access token does not // exist. -func AuthenticateByToken(ctx context.Context, token string) (*database.User, error) { - t, err := database.AccessTokens.GetBySHA1(ctx, token) +func AuthenticateByToken(store AuthStore, ctx context.Context, token string) (*database.User, error) { + t, err := store.GetAccessTokenBySHA1(ctx, token) if err != nil { 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. log.Error("Failed to touch access token [id: %d]: %v", t.ID, err) } diff --git a/internal/context/context.go b/internal/context/context.go index e90c85298..ec30640d5 100644 --- a/internal/context/context.go +++ b/internal/context/context.go @@ -235,7 +235,7 @@ func (c *Context) ServeContent(name string, r io.ReadSeeker, params ...any) { var csrfTokenExcludePattern = lazyregexp.New(`[^a-zA-Z0-9-_].*`) // 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) { c := &Context{ Context: ctx, @@ -260,7 +260,7 @@ func Contexter() macaron.Handler { } // 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 { c.IsLogged = true diff --git a/internal/context/store.go b/internal/context/store.go new file mode 100644 index 000000000..8e312bed1 --- /dev/null +++ b/internal/context/store.go @@ -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) +} diff --git a/internal/database/access_tokens.go b/internal/database/access_tokens.go index 3121bf130..b127a9a99 100644 --- a/internal/database/access_tokens.go +++ b/internal/database/access_tokens.go @@ -17,28 +17,6 @@ import ( "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. type AccessToken struct { ID int64 `gorm:"primarykey"` @@ -74,10 +52,13 @@ func (t *AccessToken) AfterFind(tx *gorm.DB) error { return nil } -var _ AccessTokensStore = (*accessTokensStore)(nil) +// AccessTokensStore is the storage layer for access tokens. +type AccessTokensStore struct { + db *gorm.DB +} -type accessTokensStore struct { - *gorm.DB +func newAccessTokensStore(db *gorm.DB) *AccessTokensStore { + return &AccessTokensStore{db} } type ErrAccessTokenAlreadyExist struct { @@ -85,19 +66,21 @@ type ErrAccessTokenAlreadyExist struct { } func IsErrAccessTokenAlreadyExist(err error) bool { - _, ok := err.(ErrAccessTokenAlreadyExist) - return ok + return errors.As(err, &ErrAccessTokenAlreadyExist{}) } func (err ErrAccessTokenAlreadyExist) Error() string { return fmt.Sprintf("access token already exists: %v", err.args) } -func (s *accessTokensStore) Create(ctx context.Context, userID int64, name string) (*AccessToken, error) { - err := s.WithContext(ctx).Where("uid = ? AND name = ?", userID, name).First(new(AccessToken)).Error +// 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. +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 { 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 } @@ -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. 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 } @@ -119,8 +102,12 @@ func (s *accessTokensStore) Create(ctx context.Context, userID int64, name strin return accessToken, nil } -func (s *accessTokensStore) DeleteByID(ctx context.Context, userID, id int64) error { - return s.WithContext(ctx).Where("id = ? AND uid = ?", id, userID).Delete(new(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. +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) @@ -132,8 +119,7 @@ type ErrAccessTokenNotExist struct { // IsErrAccessTokenNotExist returns true if the underlying error has the type // ErrAccessTokenNotExist. func IsErrAccessTokenNotExist(err error) bool { - _, ok := errors.Cause(err).(ErrAccessTokenNotExist) - return ok + return errors.As(errors.Cause(err), &ErrAccessTokenNotExist{}) } func (err ErrAccessTokenNotExist) Error() string { @@ -144,7 +130,9 @@ func (ErrAccessTokenNotExist) NotFound() bool { 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. if 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) token := new(AccessToken) - err := s.WithContext(ctx).Where("sha256 = ?", sha256).First(token).Error - if err != nil { - if err == gorm.ErrRecordNotFound { - return nil, ErrAccessTokenNotExist{args: errutil.Args{"sha": sha1}} - } + err := s.db.WithContext(ctx).Where("sha256 = ?", sha256).First(token).Error + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, ErrAccessTokenNotExist{args: errutil.Args{"sha": sha1}} + } else if err != nil { return nil, err } 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 - 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 { - return s.WithContext(ctx). +// Touch updates the updated time of the given access token to the current time. +func (s *AccessTokensStore) Touch(ctx context.Context, id int64) error { + return s.db.WithContext(ctx). Model(new(AccessToken)). Where("id = ?", id). - UpdateColumn("updated_unix", s.NowFunc().Unix()). + UpdateColumn("updated_unix", s.db.NowFunc().Unix()). Error } diff --git a/internal/database/access_tokens_test.go b/internal/database/access_tokens_test.go index 547120071..730ece899 100644 --- a/internal/database/access_tokens_test.go +++ b/internal/database/access_tokens_test.go @@ -98,13 +98,13 @@ func TestAccessTokens(t *testing.T) { t.Parallel() ctx := context.Background() - db := &accessTokensStore{ - DB: newTestDB(t, "accessTokensStore"), + s := &AccessTokensStore{ + db: newTestDB(t, "AccessTokensStore"), } for _, tc := range []struct { name string - test func(t *testing.T, ctx context.Context, db *accessTokensStore) + test func(t *testing.T, ctx context.Context, s *AccessTokensStore) }{ {"Create", accessTokensCreate}, {"DeleteByID", accessTokensDeleteByID}, @@ -114,10 +114,10 @@ func TestAccessTokens(t *testing.T) { } { t.Run(tc.name, func(t *testing.T) { t.Cleanup(func() { - err := clearTables(t, db.DB) + err := clearTables(t, s.db) require.NoError(t, err) }) - tc.test(t, ctx, db) + tc.test(t, ctx, s) }) if t.Failed() { 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" - token, err := db.Create(ctx, 1, "Test") + token, err := s.Create(ctx, 1, "Test") require.NoError(t, err) 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") // 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) - 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 - _, err = db.Create(ctx, token.UserID, token.Name) + _, err = s.Create(ctx, token.UserID, token.Name) wantErr := ErrAccessTokenAlreadyExist{ args: errutil.Args{ "userID": token.UserID, @@ -150,25 +150,25 @@ func accessTokensCreate(t *testing.T, ctx context.Context, db *accessTokensStore 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" - token, err := db.Create(ctx, 1, "Test") + token, err := s.Create(ctx, 1, "Test") require.NoError(t, err) // 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) // We should be able to get it back - _, err = db.GetBySHA1(ctx, token.Sha1) + _, err = s.GetBySHA1(ctx, token.Sha1) require.NoError(t, err) // 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) // We should get token not found error - _, err = db.GetBySHA1(ctx, token.Sha1) + _, err = s.GetBySHA1(ctx, token.Sha1) wantErr := ErrAccessTokenNotExist{ args: errutil.Args{ "sha": token.Sha1, @@ -177,17 +177,17 @@ func accessTokensDeleteByID(t *testing.T, ctx context.Context, db *accessTokensS 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" - token, err := db.Create(ctx, 1, "Test") + token, err := s.Create(ctx, 1, "Test") require.NoError(t, err) // We should be able to get it back - _, err = db.GetBySHA1(ctx, token.Sha1) + _, err = s.GetBySHA1(ctx, token.Sha1) require.NoError(t, err) // Try to get a non-existent token - _, err = db.GetBySHA1(ctx, "bad_sha") + _, err = s.GetBySHA1(ctx, "bad_sha") wantErr := ErrAccessTokenNotExist{ args: errutil.Args{ "sha": "bad_sha", @@ -196,21 +196,21 @@ func accessTokensGetBySHA(t *testing.T, ctx context.Context, db *accessTokensSto 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 - _, err := db.Create(ctx, 1, "user1_1") + _, err := s.Create(ctx, 1, "user1_1") require.NoError(t, err) - _, err = db.Create(ctx, 1, "user1_2") + _, err = s.Create(ctx, 1, "user1_2") require.NoError(t, err) // 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) // List all access tokens for user 1 - tokens, err := db.List(ctx, 1) + tokens, err := s.List(ctx, 1) 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, "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) } -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" - token, err := db.Create(ctx, 1, "Test") + token, err := s.Create(ctx, 1, "Test") require.NoError(t, err) // Updated field is zero now assert.True(t, token.Updated.IsZero()) - err = db.Touch(ctx, token.ID) + err = s.Touch(ctx, token.ID) require.NoError(t, err) // 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) - 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)) } diff --git a/internal/database/database.go b/internal/database/database.go index f0b800b41..14d9facff 100644 --- a/internal/database/database.go +++ b/internal/database/database.go @@ -50,8 +50,8 @@ var Tables = []any{ new(Notice), } -// Init initializes the database with given logger. -func Init(w logger.Writer) (*gorm.DB, error) { +// NewConnection returns a new database connection with the given logger. +func NewConnection(w logger.Writer) (*gorm.DB, error) { level := logger.Info if conf.IsProdMode() { level = logger.Warn @@ -123,7 +123,6 @@ func Init(w logger.Writer) (*gorm.DB, error) { } // Initialize stores, sorted in alphabetical order. - AccessTokens = &accessTokensStore{DB: db} Actions = NewActionsStore(db) LoginSources = &loginSourcesStore{DB: db, files: sourceFiles} LFS = &lfsStore{DB: db} @@ -136,3 +135,28 @@ func Init(w logger.Writer) (*gorm.DB, error) { 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) +} diff --git a/internal/database/mocks.go b/internal/database/mocks.go index 40c2cfc62..f412f5a9f 100644 --- a/internal/database/mocks.go +++ b/internal/database/mocks.go @@ -8,14 +8,6 @@ import ( "testing" ) -func SetMockAccessTokensStore(t *testing.T, mock AccessTokensStore) { - before := AccessTokens - AccessTokens = mock - t.Cleanup(func() { - AccessTokens = before - }) -} - func SetMockLFSStore(t *testing.T, mock LFSStore) { before := LFS LFS = mock diff --git a/internal/database/models.go b/internal/database/models.go index 6af4e9753..1ee6274de 100644 --- a/internal/database/models.go +++ b/internal/database/models.go @@ -178,24 +178,24 @@ func SetEngine() (*gorm.DB, error) { 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() if err != nil { - return err + return nil, err } 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 { - return errors.Wrap(err, "sync tables") + return nil, errors.Wrap(err, "sync tables") } - return nil + return db, nil } type Statistic struct { diff --git a/internal/route/api/v1/api.go b/internal/route/api/v1/api.go index 2b141d5a7..5503c41ee 100644 --- a/internal/route/api/v1/api.go +++ b/internal/route/api/v1/api.go @@ -186,9 +186,10 @@ func RegisterRoutes(m *macaron.Macaron) { m.Get("", user.GetInfo) m.Group("/tokens", func() { + accessTokensHandler := user.NewAccessTokensHandler(user.NewAccessTokensStore()) m.Combo(""). - Get(user.ListAccessTokens). - Post(bind(api.CreateAccessTokenOption{}), user.CreateAccessToken) + Get(accessTokensHandler.List()). + Post(bind(api.CreateAccessTokenOption{}), accessTokensHandler.Create()) }, reqBasicAuth()) }) }) diff --git a/internal/route/api/v1/user/access_tokens.go b/internal/route/api/v1/user/access_tokens.go new file mode 100644 index 000000000..b8c3062a9 --- /dev/null +++ b/internal/route/api/v1/user/access_tokens.go @@ -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) +} diff --git a/internal/route/api/v1/user/app.go b/internal/route/api/v1/user/app.go deleted file mode 100644 index 29c8a96d8..000000000 --- a/internal/route/api/v1/user/app.go +++ /dev/null @@ -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}) -} diff --git a/internal/route/install.go b/internal/route/install.go index e2ab29018..9bda5e590 100644 --- a/internal/route/install.go +++ b/internal/route/install.go @@ -71,9 +71,11 @@ func GlobalInit(customConf string) error { if conf.Security.InstallLock { highlight.NewContext() markup.NewSanitizer() - if err := database.NewEngine(); err != nil { + db, err := database.NewEngine() + if err != nil { log.Fatal("Failed to initialize ORM engine: %v", err) } + database.SetHandle(db) database.HasEngine = true database.LoadRepoConfig() diff --git a/internal/route/lfs/mocks_test.go b/internal/route/lfs/mocks_test.go index 4ab5f2f99..c4fbb1f09 100644 --- a/internal/route/lfs/mocks_test.go +++ b/internal/route/lfs/mocks_test.go @@ -14,657 +14,6 @@ import ( 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 // package gogs.io/gogs/internal/database) used for unit testing. type MockLFSStore struct { @@ -6698,3 +6047,274 @@ func (c UsersStoreUseCustomAvatarFuncCall) Args() []interface{} { func (c UsersStoreUseCustomAvatarFuncCall) Results() []interface{} { 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} +} diff --git a/internal/route/lfs/route.go b/internal/route/lfs/route.go index 25a2868d7..9ddff3e6b 100644 --- a/internal/route/lfs/route.go +++ b/internal/route/lfs/route.go @@ -19,12 +19,14 @@ import ( "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) { verifyAccept := verifyHeader("Accept", contentType, http.StatusNotAcceptable) verifyContentTypeJSON := verifyHeader("Content-Type", contentType, http.StatusBadRequest) verifyContentTypeStream := verifyHeader("Content-Type", "application/octet-stream", http.StatusBadRequest) + store := NewStore() r.Group("", func() { r.Post("/objects/batch", authorize(database.AccessModeRead), verifyAccept, verifyContentTypeJSON, serveBatch) r.Group("/objects/basic", func() { @@ -39,12 +41,12 @@ func RegisterRoutes(r *macaron.Router) { Put(authorize(database.AccessModeWrite), verifyContentTypeStream, basic.serveUpload) 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 // 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) { w.Header().Set("Lfs-Authenticate", `Basic realm="Git LFS"`) responseJSON(w, http.StatusUnauthorized, responseError{ @@ -74,14 +76,14 @@ func authenticate() macaron.Handler { // If username and password combination failed, try again using either username // or password as the token. 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) { internalServerError(c.Resp) log.Error("Failed to authenticate by access token via username: %v", err) return } else if database.IsErrAccessTokenNotExist(err) { // 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 database.IsErrAccessTokenNotExist(err) { askCredentials(c.Resp) diff --git a/internal/route/lfs/route_test.go b/internal/route/lfs/route_test.go index 8ff589531..60e21876f 100644 --- a/internal/route/lfs/route_test.go +++ b/internal/route/lfs/route_test.go @@ -20,22 +20,16 @@ import ( "gogs.io/gogs/internal/lfsutil" ) -func Test_authenticate(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) - }) - +func TestAuthenticate(t *testing.T) { tests := []struct { - name string - header http.Header - mockUsersStore func() database.UsersStore - mockTwoFactorsStore func() database.TwoFactorsStore - mockAccessTokensStore func() database.AccessTokensStore - expStatusCode int - expHeader http.Header - expBody string + name string + header http.Header + mockUsersStore func() database.UsersStore + mockTwoFactorsStore func() database.TwoFactorsStore + mockStore func() *MockStore + expStatusCode int + expHeader http.Header + expBody string }{ { name: "no authorization", @@ -75,10 +69,10 @@ func Test_authenticate(t *testing.T) { mock.AuthenticateFunc.SetDefaultReturn(nil, auth.ErrBadCredentials{}) return mock }, - mockAccessTokensStore: func() database.AccessTokensStore { - mock := NewMockAccessTokensStore() - mock.GetBySHA1Func.SetDefaultReturn(nil, database.ErrAccessTokenNotExist{}) - return mock + mockStore: func() *MockStore { + mockStore := NewMockStore() + mockStore.GetAccessTokenBySHA1Func.SetDefaultReturn(nil, database.ErrAccessTokenNotExist{}) + return mockStore }, expStatusCode: http.StatusUnauthorized, expHeader: http.Header{ @@ -118,10 +112,10 @@ func Test_authenticate(t *testing.T) { mock.GetByIDFunc.SetDefaultReturn(&database.User{ID: 1, Name: "unknwon"}, nil) return mock }, - mockAccessTokensStore: func() database.AccessTokensStore { - mock := NewMockAccessTokensStore() - mock.GetBySHA1Func.SetDefaultReturn(&database.AccessToken{}, nil) - return mock + mockStore: func() *MockStore { + mockStore := NewMockStore() + mockStore.GetAccessTokenBySHA1Func.SetDefaultReturn(&database.AccessToken{}, nil) + return mockStore }, expStatusCode: http.StatusOK, expHeader: http.Header{}, @@ -138,15 +132,15 @@ func Test_authenticate(t *testing.T) { mock.GetByIDFunc.SetDefaultReturn(&database.User{ID: 1, Name: "unknwon"}, nil) return mock }, - mockAccessTokensStore: func() database.AccessTokensStore { - mock := NewMockAccessTokensStore() - mock.GetBySHA1Func.SetDefaultHook(func(ctx context.Context, sha1 string) (*database.AccessToken, error) { + mockStore: func() *MockStore { + mockStore := NewMockStore() + mockStore.GetAccessTokenBySHA1Func.SetDefaultHook(func(_ context.Context, sha1 string) (*database.AccessToken, error) { if sha1 == "password" { return &database.AccessToken{}, nil } return nil, database.ErrAccessTokenNotExist{} }) - return mock + return mockStore }, expStatusCode: http.StatusOK, expHeader: http.Header{}, @@ -161,10 +155,16 @@ func Test_authenticate(t *testing.T) { if test.mockTwoFactorsStore != nil { database.SetMockTwoFactorsStore(t, test.mockTwoFactorsStore()) } - if test.mockAccessTokensStore != nil { - database.SetMockAccessTokensStore(t, test.mockAccessTokensStore()) + if test.mockStore == nil { + 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) if err != nil { t.Fatal(err) diff --git a/internal/route/lfs/store.go b/internal/route/lfs/store.go new file mode 100644 index 000000000..1a9bf3d3e --- /dev/null +++ b/internal/route/lfs/store.go @@ -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) +} diff --git a/internal/route/repo/http.go b/internal/route/repo/http.go index 3232fb3ff..8ba30399b 100644 --- a/internal/route/repo/http.go +++ b/internal/route/repo/http.go @@ -44,7 +44,7 @@ func askCredentials(c *macaron.Context, status int, text string) { c.Error(status, text) } -func HTTPContexter() macaron.Handler { +func HTTPContexter(store Store) macaron.Handler { return func(c *macaron.Context) { if len(conf.HTTP.AccessControlAllowOrigin) > 0 { // 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 // or password as the token. 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) { c.Status(http.StatusInternalServerError) log.Error("Failed to authenticate by access token via username: %v", err) return } else if database.IsErrAccessTokenNotExist(err) { // 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 database.IsErrAccessTokenNotExist(err) { askCredentials(c, http.StatusUnauthorized, "") diff --git a/internal/route/repo/store.go b/internal/route/repo/store.go new file mode 100644 index 000000000..e93a14ba0 --- /dev/null +++ b/internal/route/repo/store.go @@ -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) +} diff --git a/internal/route/user/setting.go b/internal/route/user/setting.go index bd24f8043..65bb7d2e5 100644 --- a/internal/route/user/setting.go +++ b/internal/route/user/setting.go @@ -6,6 +6,7 @@ package user import ( "bytes" + gocontext "context" "encoding/base64" "fmt" "html/template" @@ -15,6 +16,7 @@ import ( "github.com/pkg/errors" "github.com/pquerna/otp" "github.com/pquerna/otp/totp" + "gopkg.in/macaron.v1" log "unknwon.dev/clog/v2" "gogs.io/gogs/internal/auth" @@ -28,6 +30,18 @@ import ( "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 ( SETTINGS_PROFILE = "user/settings/profile" SETTINGS_AVATAR = "user/settings/avatar" @@ -580,62 +594,68 @@ func SettingsLeaveOrganization(c *context.Context) { }) } -func SettingsApplications(c *context.Context) { - c.Title("settings.applications") - c.PageIs("SettingsApplications") +func (h *SettingsHandler) Applications() macaron.Handler { + return func(c *context.Context) { + c.Title("settings.applications") + c.PageIs("SettingsApplications") - tokens, err := database.AccessTokens.List(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) + 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 := 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) { - if err := database.AccessTokens.DeleteByID(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")) - } +func (h *SettingsHandler) ApplicationsPost() macaron.Handler { + return func(c *context.Context, f form.NewAccessToken) { + c.Title("settings.applications") + c.PageIs("SettingsApplications") - c.JSONSuccess(map[string]any{ - "redirect": conf.Server.Subpath + "/user/settings/applications", - }) + if c.HasError() { + 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) { @@ -672,3 +692,51 @@ func SettingsDelete(c *context.Context) { 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) +} diff --git a/mockgen.yaml b/mockgen.yaml index 68b5c9c13..74bec0153 100644 --- a/mockgen.yaml +++ b/mockgen.yaml @@ -39,6 +39,8 @@ mocks: - LFSStore - UsersStore - TwoFactorsStore - - AccessTokensStore - ReposStore - PermsStore + - path: gogs.io/gogs/internal/route/lfs + interfaces: + - Store