diff --git a/controller/build.go b/controller/build.go index dacd89565..fac9be2ea 100644 --- a/controller/build.go +++ b/controller/build.go @@ -10,6 +10,7 @@ import ( log "github.com/Sirupsen/logrus" "github.com/drone/drone/engine" + "github.com/drone/drone/remote" "github.com/drone/drone/shared/httputil" "github.com/gin-gonic/gin" @@ -124,7 +125,7 @@ func DeleteBuild(c *gin.Context) { func PostBuild(c *gin.Context) { - remote := context.Remote(c) + remote_ := context.Remote(c) repo := session.Repo(c) db := context.Database(c) @@ -148,8 +149,18 @@ func PostBuild(c *gin.Context) { return } + // if the remote has a refresh token, the current access token + // may be stale. Therefore, we should refresh prior to dispatching + // the job. + if refresher, ok := remote_.(remote.Refresher); ok { + ok, _ := refresher.Refresh(user) + if ok { + model.UpdateUser(db, user) + } + } + // fetch the .drone.yml file from the database - raw, sec, err := remote.Script(user, repo, build) + raw, sec, err := remote_.Script(user, repo, build) if err != nil { log.Errorf("failure to get .drone.yml for %s. %s", repo.FullName, err) c.AbortWithError(404, err) @@ -157,7 +168,7 @@ func PostBuild(c *gin.Context) { } key, _ := model.GetKey(db, repo) - netrc, err := remote.Netrc(user, repo) + netrc, err := remote_.Netrc(user, repo) if err != nil { log.Errorf("failure to generate netrc for %s. %s", repo.FullName, err) c.AbortWithError(500, err) diff --git a/controller/hook.go b/controller/hook.go index 65b7d0e0c..5d4e45b66 100644 --- a/controller/hook.go +++ b/controller/hook.go @@ -10,6 +10,7 @@ import ( log "github.com/Sirupsen/logrus" "github.com/drone/drone/engine" "github.com/drone/drone/model" + "github.com/drone/drone/remote" "github.com/drone/drone/router/middleware/context" "github.com/drone/drone/shared/httputil" "github.com/drone/drone/shared/token" @@ -18,10 +19,10 @@ import ( ) func PostHook(c *gin.Context) { - remote := context.Remote(c) + remote_ := context.Remote(c) db := context.Database(c) - tmprepo, build, err := remote.Hook(c.Request) + tmprepo, build, err := remote_.Hook(c.Request) if err != nil { log.Errorf("failure to parse hook. %s", err) c.AbortWithError(400, err) @@ -93,8 +94,18 @@ func PostHook(c *gin.Context) { return } + // if the remote has a refresh token, the current access token + // may be stale. Therefore, we should refresh prior to dispatching + // the job. + if refresher, ok := remote_.(remote.Refresher); ok { + ok, _ := refresher.Refresh(user) + if ok { + model.UpdateUser(db, user) + } + } + // fetch the .drone.yml file from the database - raw, sec, err := remote.Script(user, repo, build) + raw, sec, err := remote_.Script(user, repo, build) if err != nil { log.Errorf("failure to get .drone.yml for %s. %s", repo.FullName, err) c.AbortWithError(404, err) @@ -111,7 +122,7 @@ func PostHook(c *gin.Context) { axes = append(axes, matrix.Axis{}) } - netrc, err := remote.Netrc(user, repo) + netrc, err := remote_.Netrc(user, repo) if err != nil { log.Errorf("failure to generate netrc for %s. %s", repo.FullName, err) c.AbortWithError(500, err) @@ -170,7 +181,7 @@ func PostHook(c *gin.Context) { c.JSON(200, build) url := fmt.Sprintf("%s/%s/%d", httputil.GetURL(c.Request), repo.FullName, build.Number) - err = remote.Status(user, repo, build, url) + err = remote_.Status(user, repo, build, url) if err != nil { log.Errorf("error setting commit status for %s/%d", repo.FullName, build.Number) } diff --git a/model/user.go b/model/user.go index 0521f268a..2931d3756 100644 --- a/model/user.go +++ b/model/user.go @@ -10,6 +10,7 @@ type User struct { Login string `json:"login" meddler:"user_login"` Token string `json:"-" meddler:"user_token"` Secret string `json:"-" meddler:"user_secret"` + Expiry int64 `json:"-" meddler:"user_expiry"` Email string `json:"email" meddler:"user_email"` Avatar string `json:"avatar_url" meddler:"user_avatar"` Active bool `json:"active," meddler:"user_active"` diff --git a/remote/bitbucket/bitbucket.go b/remote/bitbucket/bitbucket.go index ed9cf3db6..9aa6e32a4 100644 --- a/remote/bitbucket/bitbucket.go +++ b/remote/bitbucket/bitbucket.go @@ -1,7 +1,9 @@ package bitbucket import ( + "encoding/json" "fmt" + "io/ioutil" "net/http" "net/url" "strconv" @@ -80,6 +82,7 @@ func (bb *Bitbucket) Login(res http.ResponseWriter, req *http.Request) (*model.U user.Login = curr.Login user.Token = token.AccessToken user.Secret = token.RefreshToken + user.Expiry = token.Expiry.UTC().Unix() user.Avatar = curr.Links.Avatar.Href // gets the primary, confirmed email from bitbucket @@ -98,7 +101,7 @@ func (bb *Bitbucket) Login(res http.ResponseWriter, req *http.Request) (*model.U // of organizations, get the orgs and verify the // user is a member. if len(bb.Orgs) != 0 { - resp, err := client.ListTeams(&ListOpts{Page: 1, PageLen: 100}) + resp, err := client.ListTeams(&ListTeamOpts{Page: 1, PageLen: 100, Role: "member"}) if err != nil { return nil, false, err } @@ -160,6 +163,7 @@ func (bb *Bitbucket) Refresh(user *model.User) (bool, error) { // update the user to include tne new access token user.Token = token.AccessToken user.Secret = token.RefreshToken + user.Expiry = token.Expiry.UTC().Unix() return true, nil } @@ -179,43 +183,28 @@ func (bb *Bitbucket) Repo(u *model.User, owner, name string) (*model.Repo, error func (bb *Bitbucket) Repos(u *model.User) ([]*model.RepoLite, error) { token := oauth2.Token{AccessToken: u.Token, RefreshToken: u.Secret} client := NewClientToken(bb.Client, bb.Secret, &token) - - // var accounts = []string{u.Login} var repos []*model.RepoLite - // for { - // resp, err := client.ListTeams(&ListOpts{Page: page}) - // if err != nil { - // return nil, err - // } + // gets a list of all accounts to query, including the + // user's account and all team accounts. + logins := []string{u.Login} + resp, err := client.ListTeams(&ListTeamOpts{PageLen: 100, Role: "member"}) + if err != nil { + return repos, err + } + for _, team := range resp.Values { + logins = append(logins, team.Login) + } - // for _, team := range resp.Values { - // accounts = append(accounts, team.Login) - // } - - // if resp.Page == resp.Pages { - // break - // } - // } - - var page = 1 - for { - resp, err := client.ListRepos(u.Login, &ListOpts{Page: page, PageLen: 100}) + // for each account, get the list of repos + for _, login := range logins { + repos_, err := client.ListReposAll(login) if err != nil { - println(err.Error()) - return nil, err + return repos, err } - - for _, repo := range resp.Values { - repos = append(repos, convertRepoLite(&repo)) + for _, repo := range repos_ { + repos = append(repos, convertRepoLite(repo)) } - - if len(resp.Next) == 0 { - break - } - - page = resp.Page + 1 - break } return repos, nil @@ -239,7 +228,7 @@ func (bb *Bitbucket) Perm(u *model.User, owner, name string) (*model.Perm, error // if the user has access to the repository hooks we // can deduce that the user has push and admin access. - _, err = client.ListHooks(owner, name, nil) + _, err = client.ListHooks(owner, name, &ListOpts{}) if err == nil { perms.Push = true perms.Admin = true @@ -284,62 +273,132 @@ func (bb *Bitbucket) Status(u *model.User, r *model.Repo, b *model.Build, link s // Netrc returns a .netrc file that can be used to clone // private repositories from a remote system. func (bb *Bitbucket) Netrc(u *model.User, r *model.Repo) (*model.Netrc, error) { - netrc := &model.Netrc{} - netrc.Login = "x-token-auth" - netrc.Password = u.Token - netrc.Machine = "bitbucket.org" - return netrc, nil + return &model.Netrc{ + Machine: "bitbucket.org", + Login: "x-token-auth", + Password: u.Token, + }, nil } // Activate activates a repository by creating the post-commit hook and // adding the SSH deploy key, if applicable. func (bb *Bitbucket) Activate(u *model.User, r *model.Repo, k *model.Key, link string) error { + client := NewClientToken( + bb.Client, + bb.Secret, + &oauth2.Token{ + AccessToken: u.Token, + RefreshToken: u.Secret, + }, + ) - // "repo:push" - return nil + linkurl, err := url.Parse(link) + if err != nil { + return err + } + + // see if the hook already exists. If yes be sure to + // delete so that multiple messages aren't sent. + hooks, _ := client.ListHooks(r.Owner, r.Name, &ListOpts{}) + for _, hook := range hooks.Values { + hookurl, err := url.Parse(hook.Url) + if err != nil { + return err + } + if hookurl.Host == linkurl.Host { + client.DeleteHook(r.Owner, r.Name, hook.Uuid) + break + } + } + + return client.CreateHook(r.Owner, r.Name, &Hook{ + Active: true, + Desc: linkurl.Host, + Events: []string{"repo:push"}, + Url: link, + }) } // Deactivate removes a repository by removing all the post-commit hooks // which are equal to link and removing the SSH deploy key. func (bb *Bitbucket) Deactivate(u *model.User, r *model.Repo, link string) error { + client := NewClientToken( + bb.Client, + bb.Secret, + &oauth2.Token{ + AccessToken: u.Token, + RefreshToken: u.Secret, + }, + ) + + linkurl, err := url.Parse(link) + if err != nil { + return err + } + + // see if the hook already exists. If yes be sure to + // delete so that multiple messages aren't sent. + hooks, _ := client.ListHooks(r.Owner, r.Name, &ListOpts{}) + for _, hook := range hooks.Values { + hookurl, err := url.Parse(hook.Url) + if err != nil { + return err + } + if hookurl.Host == linkurl.Host { + client.DeleteHook(r.Owner, r.Name, hook.Uuid) + break + } + } + return nil } // Hook parses the post-commit hook from the Request body // and returns the required data in a standard format. func (bb *Bitbucket) Hook(r *http.Request) (*model.Repo, *model.Build, error) { - return nil, nil, nil -} -func convertRepo(from *Repo) *model.Repo { - repo := &model.Repo{} - repo.Owner = from.Owner.Login - repo.Name = from.Name - repo.FullName = from.FullName - repo.Link = from.Links.Html.Href - repo.IsPrivate = from.IsPrivate - repo.Avatar = from.Owner.Links.Avatar.Href - repo.Branch = "master" - repo.Clone = fmt.Sprintf("https://bitbucket.org/%s.git", repo.FullName) - - // above we manually constructed the repository clone url. - // below we will iterate through the list of clone links and - // attempt to instead use the clone url provided by bitbucket. - for _, link := range from.Links.Clone { - if link.Name == "https" { - repo.Clone = link.Href - break - } + // only a subset of hooks are processed by drone + if r.Header.Get("X-Event-Key") != "repo:push" { + return nil, nil, nil } - return repo -} + // extract the hook payload + payload := []byte(r.FormValue("payload")) + if len(payload) == 0 { + defer r.Body.Close() + payload, _ = ioutil.ReadAll(r.Body) + } -func convertRepoLite(from *Repo) *model.RepoLite { - repo := &model.RepoLite{} - repo.Owner = from.Owner.Login - repo.Name = from.Name - repo.FullName = from.FullName - repo.Avatar = from.Owner.Links.Avatar.Href - return repo + hook := PushHook{} + err := json.Unmarshal(payload, &hook) + if err != nil { + return nil, nil, err + } + + // the hook can container one or many changes. Since I don't + // fully understand this yet, we will just pick the first + // change that has branch information. + for _, change := range hook.Push.Changes { + + // must have branch and sha information + if change.New.Type != "branch" || change.New.Target.Hash == "" { + continue + } + + // return the updated repsitory information and the + // build information. + return convertRepo(&hook.Repo), &model.Build{ + Event: model.EventPush, + Commit: change.New.Target.Hash, + Ref: fmt.Sprintf("refs/heads/%s", change.New.Name), + Link: change.New.Target.Links.Html.Href, + Branch: change.New.Name, + Message: change.New.Target.Message, + Avatar: hook.Actor.Links.Avatar.Href, + Author: hook.Actor.Login, + Timestamp: change.New.Target.Date.UTC().Unix(), + }, nil + } + + return nil, nil, nil } diff --git a/remote/bitbucket/client.go b/remote/bitbucket/client.go index d4d141704..2fd1182fd 100644 --- a/remote/bitbucket/client.go +++ b/remote/bitbucket/client.go @@ -7,7 +7,6 @@ import ( "io" "net/http" "net/url" - "strconv" "golang.org/x/oauth2" "golang.org/x/oauth2/bitbucket" @@ -20,7 +19,18 @@ const ( del = "DELETE" ) -const api = "https://api.bitbucket.org" +const ( + base = "https://api.bitbucket.org" + + pathUser = "%s/2.0/user/" + pathEmails = "%s/2.0/user/emails" + pathTeams = "%s/2.0/teams/?%s" + pathRepo = "%s/2.0/repositories/%s/%s" + pathRepos = "%s/2.0/repositories/%s?%s" + pathHook = "%s/2.0/repositories/%s/%s/hooks/%s" + pathHooks = "%s/2.0/repositories/%s/%s/hooks?%s" + pathSource = "%s/1.0/repositories/%s/%s/src/%s/%s" +) type Client struct { *http.Client @@ -40,68 +50,87 @@ func NewClientToken(client, secret string, token *oauth2.Token) *Client { } func (c *Client) FindCurrent() (*Account, error) { - var out = new(Account) - var uri = fmt.Sprintf("%s/2.0/user/", api) - var err = c.do(uri, get, nil, out) + out := new(Account) + uri := fmt.Sprintf(pathUser, base) + err := c.do(uri, get, nil, out) return out, err } func (c *Client) ListEmail() (*EmailResp, error) { - var out = new(EmailResp) - var uri = fmt.Sprintf("%s/2.0/user/emails", api) - var err = c.do(uri, get, nil, out) + out := new(EmailResp) + uri := fmt.Sprintf(pathEmails, base) + err := c.do(uri, get, nil, out) return out, err } -func (c *Client) ListTeams(opts *ListOpts) (*AccountResp, error) { - var out = new(AccountResp) - var uri = fmt.Sprintf("%s/2.0/teams/?role=member&%s", api, encodeListOpts(opts)) - var err = c.do(uri, get, nil, out) +func (c *Client) ListTeams(opts *ListTeamOpts) (*AccountResp, error) { + out := new(AccountResp) + uri := fmt.Sprintf(pathTeams, base, opts.Encode()) + err := c.do(uri, get, nil, out) return out, err } func (c *Client) FindRepo(owner, name string) (*Repo, error) { - var out = new(Repo) - var uri = fmt.Sprintf("%s/2.0/repositories/%s/%s", api, owner, name) - var err = c.do(uri, get, nil, out) + out := new(Repo) + uri := fmt.Sprintf(pathRepo, base, owner, name) + err := c.do(uri, get, nil, out) return out, err } func (c *Client) ListRepos(account string, opts *ListOpts) (*RepoResp, error) { - var out = new(RepoResp) - var uri = fmt.Sprintf("%s/2.0/repositories/%s?%s", api, account, encodeListOpts(opts)) - var err = c.do(uri, get, nil, out) + out := new(RepoResp) + uri := fmt.Sprintf(pathRepos, base, account, opts.Encode()) + err := c.do(uri, get, nil, out) return out, err } +func (c *Client) ListReposAll(account string) ([]*Repo, error) { + var page = 1 + var repos []*Repo + + for { + resp, err := c.ListRepos(account, &ListOpts{Page: page, PageLen: 100}) + if err != nil { + println(err.Error()) + return repos, err + } + repos = append(repos, resp.Values...) + if len(resp.Next) == 0 { + break + } + page = resp.Page + 1 + } + return repos, nil +} + func (c *Client) FindHook(owner, name, id string) (*Hook, error) { - var out = new(Hook) - var uri = fmt.Sprintf("%s/2.0/repositories/%s/%s/hooks/%s", api, owner, name, id) - var err = c.do(uri, get, nil, out) + out := new(Hook) + uri := fmt.Sprintf(pathHook, base, owner, name, id) + err := c.do(uri, get, nil, out) return out, err } func (c *Client) ListHooks(owner, name string, opts *ListOpts) (*HookResp, error) { - var out = new(HookResp) - var uri = fmt.Sprintf("%s/2.0/repositories/%s/%s/hooks?%s", api, owner, name, encodeListOpts(opts)) - var err = c.do(uri, get, nil, out) + out := new(HookResp) + uri := fmt.Sprintf(pathHooks, base, owner, name, opts.Encode()) + err := c.do(uri, get, nil, out) return out, err } -func (c *Client) CreateHook(owner, name, hook *Hook) error { - var uri = fmt.Sprintf("%s/2.0/repositories/%s/%s/hooks", api, owner, name) +func (c *Client) CreateHook(owner, name string, hook *Hook) error { + uri := fmt.Sprintf(pathHooks, base, owner, name, "") return c.do(uri, post, hook, nil) } func (c *Client) DeleteHook(owner, name, id string) error { - var uri = fmt.Sprintf("%s/2.0/repositories/%s/%s/hooks/%s", api, owner, name, id) + uri := fmt.Sprintf(pathHook, base, owner, name, id) return c.do(uri, del, nil, nil) } func (c *Client) FindSource(owner, name, revision, path string) (*Source, error) { - var out = new(Source) - var uri = fmt.Sprintf("%s/1.0/repositories/%s/%s/src/%s/%s", api, owner, name, revision, path) - var err = c.do(uri, get, nil, out) + out := new(Source) + uri := fmt.Sprintf(pathSource, base, owner, name, revision, path) + err := c.do(uri, get, nil, out) return out, err } @@ -128,6 +157,9 @@ func (c *Client) do(rawurl, method string, in, out interface{}) error { if err != nil { return err } + if in != nil { + req.Header.Set("Content-Type", "application/json") + } resp, err := c.Do(req) if err != nil { @@ -141,6 +173,11 @@ func (c *Client) do(rawurl, method string, in, out interface{}) error { err := Error{} json.NewDecoder(resp.Body).Decode(&err) err.Status = resp.StatusCode + + instr, _ := json.Marshal(in) + println(err.Body.Message) + println(string(instr)) + println(uri.String()) return err } @@ -152,17 +189,3 @@ func (c *Client) do(rawurl, method string, in, out interface{}) error { return nil } - -func encodeListOpts(opts *ListOpts) string { - var params = new(url.Values) - if opts == nil { - return params.Encode() - } - if opts.Page != 0 { - params.Set("page", strconv.Itoa(opts.Page)) - } - if opts.PageLen != 0 { - params.Set("pagelen", strconv.Itoa(opts.PageLen)) - } - return params.Encode() -} diff --git a/remote/bitbucket/helper.go b/remote/bitbucket/helper.go new file mode 100644 index 000000000..c417857ab --- /dev/null +++ b/remote/bitbucket/helper.go @@ -0,0 +1,56 @@ +package bitbucket + +import ( + "strings" + + "github.com/drone/drone/model" +) + +// convertRepo is a helper function used to convert a Bitbucket +// repository structure to the common Drone repository structure. +func convertRepo(from *Repo) *model.Repo { + repo := model.Repo{ + Owner: from.Owner.Login, + Name: from.Name, + FullName: from.FullName, + Link: from.Links.Html.Href, + IsPrivate: from.IsPrivate, + Avatar: from.Owner.Links.Avatar.Href, + Branch: "master", + } + + // in some cases, the owner of the repository is not + // provided, however, we do have the full name. + if len(repo.Owner) == 0 { + repo.Owner = strings.Split(repo.FullName, "/")[0] + } + + // above we manually constructed the repository clone url. + // below we will iterate through the list of clone links and + // attempt to instead use the clone url provided by bitbucket. + for _, link := range from.Links.Clone { + if link.Name == "https" { + repo.Clone = link.Href + break + } + } + + // if no repository name is provided, we use the Html link. + // this excludes the .git suffix, but will still clone the repo. + if len(repo.Clone) == 0 { + repo.Clone = repo.Link + } + + return &repo +} + +// convertRepoLite is a helper function used to convert a Bitbucket +// repository structure to the simplified Drone repository structure. +func convertRepoLite(from *Repo) *model.RepoLite { + return &model.RepoLite{ + Owner: from.Owner.Login, + Name: from.Name, + FullName: from.FullName, + Avatar: from.Owner.Links.Avatar.Href, + } +} diff --git a/remote/bitbucket/types.go b/remote/bitbucket/types.go index 1ff14d385..abe409424 100644 --- a/remote/bitbucket/types.go +++ b/remote/bitbucket/types.go @@ -1,5 +1,11 @@ package bitbucket +import ( + "net/url" + "strconv" + "time" +) + type Account struct { Login string `json:"username"` Name string `json:"display_name"` @@ -8,11 +14,11 @@ type Account struct { } type AccountResp struct { - Page int `json:"page"` - Pages int `json:"pagelen"` - Size int `json:"size"` - Next string `json:"next"` - Values []Account `json:"values"` + Page int `json:"page"` + Pages int `json:"pagelen"` + Size int `json:"size"` + Next string `json:"next"` + Values []*Account `json:"values"` } type Email struct { @@ -22,11 +28,11 @@ type Email struct { } type EmailResp struct { - Page int `json:"page"` - Pages int `json:"pagelen"` - Size int `json:"size"` - Next string `json:"next"` - Values []Email `json:"values"` + Page int `json:"page"` + Pages int `json:"pagelen"` + Size int `json:"size"` + Next string `json:"next"` + Values []*Email `json:"values"` } type Hook struct { @@ -38,11 +44,11 @@ type Hook struct { } type HookResp struct { - Page int `json:"page"` - Pages int `json:"pagelen"` - Size int `json:"size"` - Next string `json:"next"` - Values []Hook `json:"values"` + Page int `json:"page"` + Pages int `json:"pagelen"` + Size int `json:"size"` + Next string `json:"next"` + Values []*Hook `json:"values"` } type Links struct { @@ -72,11 +78,11 @@ type Repo struct { } type RepoResp struct { - Page int `json:"page"` - Pages int `json:"pagelen"` - Size int `json:"size"` - Next string `json:"next"` - Values []Repo `json:"values"` + Page int `json:"page"` + Pages int `json:"pagelen"` + Size int `json:"size"` + Next string `json:"next"` + Values []*Repo `json:"values"` } type Source struct { @@ -86,11 +92,66 @@ type Source struct { Size int64 `json:"size"` } +type PushHook struct { + Actor Account `json:"actor"` + Repo Repo `json:"repository"` + Push struct { + Changes []struct { + New struct { + Type string `json:"type"` + Name string `json:"name"` + Target struct { + Type string `json:"type"` + Hash string `json:"hash"` + Message string `json:"message"` + Date time.Time `json:"date"` + Links Links `json:"links"` + Author struct { + Raw string `json:"raw"` + User Account `json:"user"` + } `json:"author"` + } `json:"target"` + } `json:"new"` + } `json:"changes"` + } `json:"push"` +} + type ListOpts struct { Page int PageLen int } +func (o *ListOpts) Encode() string { + params := new(url.Values) + if o.Page != 0 { + params.Set("page", strconv.Itoa(o.Page)) + } + if o.PageLen != 0 { + params.Set("pagelen", strconv.Itoa(o.PageLen)) + } + return params.Encode() +} + +type ListTeamOpts struct { + Page int + PageLen int + Role string +} + +func (o *ListTeamOpts) Encode() string { + params := new(url.Values) + if o.Page != 0 { + params.Set("page", strconv.Itoa(o.Page)) + } + if o.PageLen != 0 { + params.Set("pagelen", strconv.Itoa(o.PageLen)) + } + if len(o.Role) != 0 { + params.Set("role", o.Role) + } + return params.Encode() +} + type Error struct { Status int Body struct { diff --git a/router/middleware/refresh/refresh.go b/router/middleware/refresh/refresh.go new file mode 100644 index 000000000..ec3843c3f --- /dev/null +++ b/router/middleware/refresh/refresh.go @@ -0,0 +1,55 @@ +package refresh + +import ( + "time" + + "github.com/drone/drone/model" + "github.com/drone/drone/remote" + "github.com/drone/drone/router/middleware/context" + "github.com/drone/drone/router/middleware/session" + + log "github.com/Sirupsen/logrus" + "github.com/gin-gonic/gin" +) + +func Refresh(c *gin.Context) { + user := session.User(c) + if user == nil || user.Expiry == 0 { + c.Next() + return + } + + db := context.Database(c) + remote_ := context.Remote(c) + + // check if the remote includes the ability to + // refresh the user token. + refresher, ok := remote_.(remote.Refresher) + if !ok { + c.Next() + return + } + + // check to see if the user token is expired or + // will expire within the next 30 minutes (1800 seconds). + // If not, there is nothing we really need to do here. + if time.Now().UTC().Unix() > (user.Expiry - 1800) { + c.Next() + return + } + + // attempts to refresh the access token. If the + // token is refreshed, we must also persist to the + // database. + ok, _ = refresher.Refresh(user) + if ok { + err := model.UpdateUser(db, user) + if err != nil { + // we only log the error at this time. not sure + // if we really want to fail the request, do we? + log.Errorf("cannot refresh access token for %s. %s", user.Login, err) + } + } + + c.Next() +} diff --git a/router/router.go b/router/router.go index 9fcbc26bf..fe440b63c 100644 --- a/router/router.go +++ b/router/router.go @@ -8,6 +8,7 @@ import ( "github.com/drone/drone/controller" "github.com/drone/drone/router/middleware/header" + "github.com/drone/drone/router/middleware/refresh" "github.com/drone/drone/router/middleware/session" "github.com/drone/drone/static" "github.com/drone/drone/template" @@ -21,6 +22,7 @@ func Load(middleware ...gin.HandlerFunc) http.Handler { e.Use(header.SetHeaders()) e.Use(middleware...) e.Use(session.SetUser()) + e.Use(refresh.Refresh) e.GET("/", controller.ShowIndex) e.GET("/login", controller.ShowLogin)