Merge remote-tracking branch 'upstream/master' into rm-basic-auth-gitea

This commit is contained in:
Matti R 2019-06-24 19:34:27 -04:00
commit 8ad5123479
No known key found for this signature in database
GPG key ID: 9D8A57ADAA232E95
21 changed files with 319 additions and 22 deletions

36
.github/security.md vendored Normal file
View file

@ -0,0 +1,36 @@
# Security Policies and Procedures
This document outlines security procedures and general policies for this project.
* [Reporting a Bug](#reporting-a-bug)
* [Disclosure Policy](#disclosure-policy)
* [Comments on this Policy](#comments-on-this-policy)
## Reporting a Bug
Report security bugs by emailing the lead maintainer at security@drone.io.
The lead maintainer will acknowledge your email within 48 hours, and will send a
more detailed response within 48 hours indicating the next steps in handling
your report. After the initial reply to your report, the security team will
endeavor to keep you informed of the progress towards a fix and full
announcement, and may ask for additional information or guidance.
Report security bugs in third-party packages to the person or team maintaining
the module.
## Disclosure Policy
When the security team receives a security bug report, they will assign it to a
primary handler. This person will coordinate the fix and release process,
involving the following steps:
* Confirm the problem and determine the affected versions.
* Audit code to find any potential similar problems.
* Prepare fixes for all releases still under maintenance. These fixes will be
released as fast as possible to npm.
## Comments on this Policy
If you have suggestions on how this process could be improved please submit a
pull request.

View file

@ -5,10 +5,19 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## Unreleased
## [1.2.1] - 2019-06-11
### Added
- support for legacy tokens to ease upgrade path, by [@bradrydzewski](https://github.com/bradrydzewski). [#2713](https://github.com/drone/drone/issues/2713).
- include repository name and id in batch update error message, by [@bradrydzewski](https://github.com/bradrydzewski).
### Fixed
- fix inconsistent base64 encoding and decoding of encrypted secrets, by [@bradrydzewski](https://github.com/bradrydzewski).
- update drone-yaml to version 1.1.2 for improved 0.8 to 1.0 yaml marshal escaping.
- update drone-yaml to version 1.1.3 for improved 0.8 to 1.0 workspace conversion.
## [1.2.0] - 2019-05-30
### Added

View file

@ -259,9 +259,10 @@ type (
// Session provides the session configuration.
Session struct {
Timeout time.Duration `envconfig:"DRONE_COOKIE_TIMEOUT" default:"720h"`
Secret string `envconfig:"DRONE_COOKIE_SECRET"`
Secure bool `envconfig:"DRONE_COOKIE_SECURE"`
Timeout time.Duration `envconfig:"DRONE_COOKIE_TIMEOUT" default:"720h"`
Secret string `envconfig:"DRONE_COOKIE_SECRET"`
Secure bool `envconfig:"DRONE_COOKIE_SECURE"`
MappingFile string `envconfig:"DRONE_LEGACY_TOKEN_MAPPING_FILE"`
}
// Status provides status configurations.

View file

@ -92,12 +92,21 @@ func provideNetrcService(client *scm.Client, renewer core.Renewer, config config
// provideSession is a Wire provider function that returns a
// user session based on the environment configuration.
func provideSession(store core.UserStore, config config.Config) core.Session {
func provideSession(store core.UserStore, config config.Config) (core.Session, error) {
if config.Session.MappingFile != "" {
return session.Legacy(store, session.Config{
Secure: config.Session.Secure,
Secret: config.Session.Secret,
Timeout: config.Session.Timeout,
MappingFile: config.Session.MappingFile,
})
}
return session.New(store, session.NewConfig(
config.Session.Secret,
config.Session.Timeout,
config.Session.Secure),
)
), nil
}
// provideUserService is a Wire provider function that returns a

View file

@ -80,7 +80,10 @@ func InitializeApplication(config2 config.Config) (application, error) {
licenseService := license.NewService(userStore, repositoryStore, buildStore, coreLicense)
permStore := perm.New(db)
repositoryService := repo.New(client, renewer)
session := provideSession(userStore, config2)
session, err := provideSession(userStore, config2)
if err != nil {
return application{}, err
}
batcher := batch.New(db)
syncer := provideSyncer(repositoryService, repositoryStore, userStore, batcher, config2)
server := api.New(buildStore, commitService, cronStore, corePubsub, globalSecretStore, hookService, logStore, coreLicense, licenseService, permStore, repositoryStore, repositoryService, scheduler, secretStore, stageStore, stepStore, statusService, session, logStream, syncer, system, triggerer, userStore, webhookSender)

5
go.mod
View file

@ -12,17 +12,18 @@ require (
github.com/davecgh/go-spew v1.1.1
github.com/dchest/authcookie v0.0.0-20120917135355-fbdef6e99866
github.com/dchest/uniuri v0.0.0-20160212164326-8902c56451e9
github.com/dgrijalva/jwt-go v3.2.0+incompatible
github.com/docker/distribution v2.7.1+incompatible
github.com/docker/go-connections v0.3.0
github.com/docker/go-units v0.3.3
github.com/drone/drone-go v1.0.5-0.20190427184118-618e4496482e
github.com/drone/drone-runtime v1.0.6
github.com/drone/drone-ui v0.0.0-20190530175131-92ba3df1e0a9
github.com/drone/drone-yaml v1.1.1
github.com/drone/drone-yaml v1.1.4-0.20190614011118-4889634ea9ae
github.com/drone/envsubst v1.0.1
github.com/drone/go-license v1.0.2
github.com/drone/go-login v1.0.4-0.20190311170324-2a4df4f242a2
github.com/drone/go-scm v1.4.1-0.20190418181654-1e77204716f6
github.com/drone/go-scm v1.5.0
github.com/drone/signal v1.0.0
github.com/dustin/go-humanize v1.0.0
github.com/ghodss/yaml v1.0.0

12
go.sum
View file

@ -26,6 +26,8 @@ github.com/dchest/authcookie v0.0.0-20120917135355-fbdef6e99866 h1:98WJ4YCdjmB7u
github.com/dchest/authcookie v0.0.0-20120917135355-fbdef6e99866/go.mod h1:x7AK2h2QzaXVEFi1tbMYMDuvHcCEr1QdMDrg3hkW24Q=
github.com/dchest/uniuri v0.0.0-20160212164326-8902c56451e9 h1:74lLNRzvsdIlkTgfDSMuaPjBr4cf6k7pwQQANm/yLKU=
github.com/dchest/uniuri v0.0.0-20160212164326-8902c56451e9/go.mod h1:GgB8SF9nRG+GqaDtLcwJZsQFhcogVCJ79j4EdT0c2V4=
github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM=
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
github.com/docker/distribution v0.0.0-20170726174610-edc3ab29cdff/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
github.com/docker/distribution v2.7.1+incompatible h1:a5mlkVzth6W5A4fOsS3D2EO5BUmsJpcB+cRlLU7cSug=
github.com/docker/distribution v2.7.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
@ -75,6 +77,14 @@ github.com/drone/drone-yaml v1.1.0 h1:vY5AHfYtGTJD7mYtqtROiEpfjjjxnhbRfDxLfQ0zPz
github.com/drone/drone-yaml v1.1.0/go.mod h1:l/ehbHx9TGs4jgzhRnP5d+M9tmRsAmWyBHWAFEOXrk4=
github.com/drone/drone-yaml v1.1.1 h1:i2k4gVCDLN/NhfjqWQK6YLWllkDgaOGZZ4TygctQE9E=
github.com/drone/drone-yaml v1.1.1/go.mod h1:l/ehbHx9TGs4jgzhRnP5d+M9tmRsAmWyBHWAFEOXrk4=
github.com/drone/drone-yaml v1.1.2 h1:mXW9eGd8QCcwtRJ2GQ0Ll+6tuUs5P3dE107ow1wGkK4=
github.com/drone/drone-yaml v1.1.2/go.mod h1:l/ehbHx9TGs4jgzhRnP5d+M9tmRsAmWyBHWAFEOXrk4=
github.com/drone/drone-yaml v1.1.3 h1:fCtt8RcCQDyqHK5vbbMnTpwYoIspeDErhw4HNWN9I1I=
github.com/drone/drone-yaml v1.1.3/go.mod h1:l/ehbHx9TGs4jgzhRnP5d+M9tmRsAmWyBHWAFEOXrk4=
github.com/drone/drone-yaml v1.1.4-0.20190610220437-a338c245d7d7 h1:xpnCbq/pHAYLyIhcxsKIvQtyv26Zr/S7nrke6vQgy0c=
github.com/drone/drone-yaml v1.1.4-0.20190610220437-a338c245d7d7/go.mod h1:l/ehbHx9TGs4jgzhRnP5d+M9tmRsAmWyBHWAFEOXrk4=
github.com/drone/drone-yaml v1.1.4-0.20190614011118-4889634ea9ae h1:CzpY1F9Ju46gw4lbXwXrZCGyiAuP8QAlYtMcwYTVtW8=
github.com/drone/drone-yaml v1.1.4-0.20190614011118-4889634ea9ae/go.mod h1:l/ehbHx9TGs4jgzhRnP5d+M9tmRsAmWyBHWAFEOXrk4=
github.com/drone/envsubst v1.0.1 h1:NOOStingM2sbBwsIUeQkKUz8ShwCUzmqMxWrpXItfPE=
github.com/drone/envsubst v1.0.1/go.mod h1:bkZbnc/2vh1M12Ecn7EYScpI4YGYU0etwLJICOWi8Z0=
github.com/drone/go-license v1.0.2 h1:7OwndfYk+Lp/cGHkxe4HUn/Ysrrw3WYH2pnd99yrkok=
@ -93,6 +103,8 @@ github.com/drone/go-scm v1.4.0 h1:sYkPvIQb0tVuct/zX+KfAn88I6qPnSkoktRD4hUDkzY=
github.com/drone/go-scm v1.4.0/go.mod h1:YT4FxQ3U/ltdCrBJR9B0tRpJ1bYA/PM3NyaLE/rYIvw=
github.com/drone/go-scm v1.4.1-0.20190418181654-1e77204716f6 h1:xQ0riJTnWFLZcpXry5RVOk18DBJPhUkQjIALQCDdwZ4=
github.com/drone/go-scm v1.4.1-0.20190418181654-1e77204716f6/go.mod h1:YT4FxQ3U/ltdCrBJR9B0tRpJ1bYA/PM3NyaLE/rYIvw=
github.com/drone/go-scm v1.5.0 h1:Hn3bFYsUgOEUCx2wt2II9CxkTfev2h+tPheuYHp7ehg=
github.com/drone/go-scm v1.5.0/go.mod h1:YT4FxQ3U/ltdCrBJR9B0tRpJ1bYA/PM3NyaLE/rYIvw=
github.com/drone/signal v1.0.0 h1:NrnM2M/4yAuU/tXs6RP1a1ZfxnaHwYkd0kJurA1p6uI=
github.com/drone/signal v1.0.0/go.mod h1:S8t92eFT0g4WUgEc/LxG+LCuiskpMNsG0ajAMGnyZpc=
github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo=

View file

@ -185,7 +185,7 @@ func (s Server) Handler() http.Handler {
).Delete("/{number}", builds.HandleCancel(s.Users, s.Repos, s.Builds, s.Stages, s.Steps, s.Status, s.Scheduler, s.Webhook))
r.With(
acl.CheckAdminAccess(),
acl.CheckWriteAccess(),
).Post("/{number}/promote", builds.HandlePromote(s.Repos, s.Builds, s.Triggerer))
// r.With(

View file

@ -64,7 +64,7 @@ func Handler(repos core.RepositoryStore) http.HandlerFunc {
// the encrypted secret is embedded in the yaml
// configuration file and is json-encoded for
// inclusion as a !binary attribute.
encoded := base64.URLEncoding.EncodeToString(encrypted)
encoded := base64.StdEncoding.EncodeToString(encrypted)
render.JSON(w, &respEncrypted{Data: encoded}, 200)
}

View file

@ -138,6 +138,9 @@ func (t *teardown) do(ctx context.Context, stage *core.Stage) error {
break
}
}
if build.Started == 0 {
build.Started = build.Finished
}
err = t.Builds.Update(noContext, build)
if err == db.ErrOptimisticLock {

View file

@ -12,6 +12,7 @@ import (
"github.com/drone/drone-yaml/yaml"
"github.com/drone/drone/core"
"github.com/drone/drone/logger"
"github.com/drone/drone-go/drone"
"github.com/drone/drone-go/plugin/secret"
@ -37,12 +38,17 @@ func (c *externalController) Find(ctx context.Context, in *core.SecretArgs) (*co
return nil, nil
}
logger := logger.FromContext(ctx).
WithField("name", in.Name).
WithField("kind", "secret")
// lookup the named secret in the manifest. If the
// secret does not exist, return a nil variable,
// allowing the next secret controller in the chain
// to be invoked.
path, name, ok := getExternal(in.Conf, in.Name)
if !ok {
logger.Trace("secret: external: no matching secret")
return nil, nil
}
@ -62,6 +68,7 @@ func (c *externalController) Find(ctx context.Context, in *core.SecretArgs) (*co
client := secret.Client(c.endpoint, c.secret, c.skipVerify)
res, err := client.Find(ctx, req)
if err != nil {
logger.WithError(err).Trace("secret: external: cannot get secret")
return nil, err
}
@ -69,6 +76,7 @@ func (c *externalController) Find(ctx context.Context, in *core.SecretArgs) (*co
// this indicates the client returned No Content,
// and we should exit with no secret, but no error.
if res.Data == "" {
logger.Trace("secret: external: secret disabled for pull requests")
return nil, nil
}
@ -77,9 +85,12 @@ func (c *externalController) Find(ctx context.Context, in *core.SecretArgs) (*co
// empty results.
if (res.Pull == false && res.PullRequest == false) &&
in.Build.Event == core.EventPullRequest {
logger.Trace("secret: external: restricted from forks")
return nil, nil
}
logger.Trace("secret: external: found matching secret")
return &core.Secret{
Name: in.Name,
Data: res.Data,

View file

@ -131,6 +131,8 @@ func (p *parser) Parse(req *http.Request, secretFunc func(string) string) (*core
Message: v.Commit.Message,
Before: v.Before,
After: v.Commit.Sha,
Source: scm.TrimRef(v.BaseRef),
Target: scm.TrimRef(v.BaseRef),
Ref: v.Ref,
Author: v.Commit.Author.Login,
AuthorName: v.Commit.Author.Name,

View file

@ -101,5 +101,9 @@ func Load(path string) (*core.License, error) {
return nil, err
}
if license.Users == 0 && decoded.Lim > 0 {
license.Users = int64(decoded.Lim)
}
return license, err
}

View file

@ -18,9 +18,10 @@ import "time"
// Config provides the session configuration.
type Config struct {
Secure bool
Secret string
Timeout time.Duration
Secure bool
Secret string
Timeout time.Duration
MappingFile string
}
// NewConfig returns a new session configuration.

108
session/legacy.go Normal file
View file

@ -0,0 +1,108 @@
// Copyright 2019 Drone IO, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package session
import (
"encoding/json"
"errors"
"io/ioutil"
"net/http"
"github.com/drone/drone/core"
"github.com/dgrijalva/jwt-go"
)
type legacy struct {
*session
mapping map[string]string
}
// Legacy returns a session manager that is capable of mapping
// legacy tokens to 1.0 users using a mapping file.
func Legacy(users core.UserStore, config Config) (core.Session, error) {
base := &session{
secret: []byte(config.Secret),
secure: config.Secure,
timeout: config.Timeout,
users: users,
}
out, err := ioutil.ReadFile(config.MappingFile)
if err != nil {
return nil, err
}
mapping := map[string]string{}
err = json.Unmarshal(out, &mapping)
if err != nil {
return nil, err
}
return &legacy{base, mapping}, nil
}
func (s *legacy) Get(r *http.Request) (*core.User, error) {
switch {
case isAuthorizationToken(r):
return s.fromToken(r)
case isAuthorizationParameter(r):
return s.fromToken(r)
default:
return s.fromSession(r)
}
}
func (s *legacy) fromToken(r *http.Request) (*core.User, error) {
extracted := extractToken(r)
// determine if the token is a legacy token based on length.
// legacy tokens are > 64 characters.
if len(extracted) < 64 {
return s.users.FindToken(r.Context(), extracted)
}
token, err := jwt.Parse(extracted, func(token *jwt.Token) (interface{}, error) {
// validate the signing method
_, ok := token.Method.(*jwt.SigningMethodHMAC)
if !ok {
return nil, errors.New("Legacy token: invalid signature")
}
claims, ok := token.Claims.(jwt.MapClaims)
if !ok {
return nil, errors.New("Legacy token: invalid claim format")
}
// extract the username claim
claim, ok := claims["text"]
if !ok {
return nil, errors.New("Legacy token: invalid format")
}
// lookup the username to get the secret
secret, ok := s.mapping[claim.(string)]
if !ok {
return nil, errors.New("Legacy token: cannot lookup user")
}
return []byte(secret), nil
})
if err != nil {
return nil, err
}
return s.users.FindLogin(
r.Context(),
token.Claims.(jwt.MapClaims)["text"].(string),
)
}

93
session/legacy_test.go Normal file
View file

@ -0,0 +1,93 @@
// Copyright 2019 Drone.IO Inc. All rights reserved.
// Use of this source code is governed by the Drone Non-Commercial License
// that can be found in the LICENSE file.
// +build !oss
package session
import (
"net/http/httptest"
"testing"
"time"
"github.com/drone/drone/core"
"github.com/drone/drone/mock"
"github.com/golang/mock/gomock"
)
func TestLegacyGet_NotLegacy(t *testing.T) {
controller := gomock.NewController(t)
defer controller.Finish()
mockUser := &core.User{
Login: "octocat",
Hash: "ulSxuA0FKjNiOFIchk18NNvC6ygSxdtKjiOAS",
}
users := mock.NewMockUserStore(controller)
users.EXPECT().FindToken(gomock.Any(), mockUser.Hash).Return(mockUser, nil)
r := httptest.NewRequest("GET", "/", nil)
r.Header.Set("Authorization", "Bearer ulSxuA0FKjNiOFIchk18NNvC6ygSxdtKjiOAS")
session, _ := Legacy(users, Config{Secure: false, Timeout: time.Hour, MappingFile: "testdata/mapping.json"})
user, _ := session.Get(r)
if user != mockUser {
t.Errorf("Want authenticated user")
}
}
func TestLegacyGet(t *testing.T) {
controller := gomock.NewController(t)
defer controller.Finish()
mockUser := &core.User{
Login: "octocat",
Hash: "ulSxuA0FKjNiOFIchk18NNvC6ygSxdtKjiOAS",
}
users := mock.NewMockUserStore(controller)
users.EXPECT().FindLogin(gomock.Any(), gomock.Any()).Return(mockUser, nil)
r := httptest.NewRequest("GET", "/?access_token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwidGV4dCI6Im9jdG9jYXQiLCJpYXQiOjE1MTYyMzkwMjJ9.jf17GpOuKu-KAhuvxtjVvmZfwyeC7mEpKNiM6_cGOvo", nil)
session, _ := Legacy(users, Config{Secure: false, Timeout: time.Hour, MappingFile: "testdata/mapping.json"})
user, err := session.Get(r)
if err != nil {
t.Error(err)
return
}
if user != mockUser {
t.Errorf("Want authenticated user")
}
}
func TestLegacyGet_UserNotFound(t *testing.T) {
controller := gomock.NewController(t)
defer controller.Finish()
users := mock.NewMockUserStore(controller)
r := httptest.NewRequest("GET", "/?access_token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwidGV4dCI6ImJpbGx5aWRvbCIsImlhdCI6MTUxNjIzOTAyMn0.yxTCucstDM7BaixXBMAJCXup9zBaFr02Kalv_PqCDM4", nil)
session, _ := Legacy(users, Config{Secure: false, Timeout: time.Hour, MappingFile: "testdata/mapping.json"})
_, err := session.Get(r)
if err == nil || err.Error() != "Legacy token: cannot lookup user" {
t.Errorf("Expect user lookup error, got %v", err)
return
}
}
func TestLegacyGet_InvalidSignature(t *testing.T) {
controller := gomock.NewController(t)
defer controller.Finish()
users := mock.NewMockUserStore(controller)
r := httptest.NewRequest("GET", "/?access_token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwidGV4dCI6InNwYWNlZ2hvc3QiLCJpYXQiOjE1MTYyMzkwMjJ9.jlGcn2WI_oEZyLqYrvNvDXNbG3H3rqMyqQI2Gc6CHIY", nil)
session, _ := Legacy(users, Config{Secure: false, Timeout: time.Hour, MappingFile: "testdata/mapping.json"})
_, err := session.Get(r)
if err == nil || err.Error() != "signature is invalid" {
t.Errorf("Expect user lookup error, got %v", err)
return
}
}

View file

@ -111,7 +111,7 @@ func TestGet_Cookie(t *testing.T) {
Name: "_session_",
Value: s,
})
session := New(users, Config{false, secret, time.Hour})
session := New(users, Config{Secure: false, Secret: secret, Timeout: time.Hour})
user, err := session.Get(r)
if err != nil {
t.Error(err)
@ -162,7 +162,7 @@ func TestGet_Cookie_UserNotFound(t *testing.T) {
Value: s,
})
session := New(users, Config{false, secret, time.Hour})
session := New(users, Config{Secure: false, Secret: secret, Timeout: time.Hour})
user, _ := session.Get(r)
if user != nil {
t.Errorf("Expect empty session")

4
session/testdata/mapping.json vendored Normal file
View file

@ -0,0 +1,4 @@
{
"octocat": "this-is-a-test-secret",
"spaceghost": "this-is-an-invalid-secret"
}

View file

@ -77,7 +77,7 @@ func (b *batchUpdater) Batch(ctx context.Context, user *core.User, batch *core.B
}
_, err = execer.Exec(stmt, args...)
if err != nil {
return fmt.Errorf("Error inserting repository: %s", err)
return fmt.Errorf("Error inserting repository: %s: %s: %s", repo.Slug, repo.UID, err)
}
//
@ -100,7 +100,7 @@ func (b *batchUpdater) Batch(ctx context.Context, user *core.User, batch *core.B
now,
)
if err != nil {
return fmt.Errorf("Error inserting permissions: %s", err)
return fmt.Errorf("Error inserting permissions: %s: %s: %s", repo.Slug, repo.UID, err)
}
}
@ -117,7 +117,7 @@ func (b *batchUpdater) Batch(ctx context.Context, user *core.User, batch *core.B
}
_, err = execer.Exec(stmt, args...)
if err != nil {
return fmt.Errorf("Error updating repository: %s", err)
return fmt.Errorf("Error updating repository: %s: %s: %s", repo.Slug, repo.UID, err)
}
stmt = permInsertIgnoreStmt
@ -135,7 +135,7 @@ func (b *batchUpdater) Batch(ctx context.Context, user *core.User, batch *core.B
now,
)
if err != nil {
return fmt.Errorf("Error inserting permissions: %s", err)
return fmt.Errorf("Error inserting permissions: %s: %s: %s", repo.Slug, repo.UID, err)
}
}
@ -153,7 +153,7 @@ func (b *batchUpdater) Batch(ctx context.Context, user *core.User, batch *core.B
_, err = execer.Exec(stmt, user.ID, repo.UID)
if err != nil {
return fmt.Errorf("Error revoking permissions: %s", err)
return fmt.Errorf("Error revoking permissions: %s: %s: %s", repo.Slug, repo.UID, err)
}
}

View file

@ -27,7 +27,7 @@ var (
// VersionMinor is for functionality in a backwards-compatible manner.
VersionMinor int64 = 2
// VersionPatch is for backwards-compatible bug fixes.
VersionPatch int64
VersionPatch int64 = 1
// VersionPre indicates prerelease.
VersionPre = ""
// VersionDev indicates development branch. Releases will be empty string.

View file

@ -9,7 +9,7 @@ package version
import "testing"
func TestVersion(t *testing.T) {
if got, want := Version.String(), "1.2.0"; got != want {
if got, want := Version.String(), "1.2.1"; got != want {
t.Errorf("Want version %s, got %s", want, got)
}
}