func GetRepos(c *gin.Context) { user := session.User(c) remote := remote.FromContext(c) var repos []*model.RepoLite // get the repository list from the cache reposv, ok := c.Get("repos") if ok { repos = reposv.([]*model.RepoLite) } else { var err error repos, err = remote.Repos(user) if err != nil { c.AbortWithStatus(http.StatusInternalServerError) return } } // for each repository in the remote system we get // the intersection of those repostiories in Drone repos_, err := store.GetRepoListOf(c, repos) if err != nil { c.AbortWithStatus(http.StatusInternalServerError) return } c.Set("repos", repos) c.IndentedJSON(http.StatusOK, repos_) }
func GetFeed(c *gin.Context) { user := session.User(c) remote := remote.FromContext(c) var repos []*model.RepoLite // get the repository list from the cache reposv, ok := c.Get("repos") if ok { repos = reposv.([]*model.RepoLite) } else { var err error repos, err = remote.Repos(user) if err != nil { c.String(400, err.Error()) return } } feed, err := store.GetUserFeed(c, repos) if err != nil { c.String(400, err.Error()) return } c.JSON(200, feed) }
func GetLoginToken(c *gin.Context) { remote := remote.FromContext(c) in := &tokenPayload{} err := c.Bind(in) if err != nil { c.AbortWithError(http.StatusBadRequest, err) return } login, err := remote.Auth(in.Access, in.Refresh) if err != nil { c.AbortWithError(http.StatusUnauthorized, err) return } user, err := store.GetUserLogin(c, login) if err != nil { c.AbortWithError(http.StatusNotFound, err) return } exp := time.Now().Add(time.Hour * 72).Unix() token := token.New(token.SessToken, user.Login) tokenstr, err := token.SignExpires(user.Hash, exp) if err != nil { c.AbortWithError(http.StatusInternalServerError, err) return } c.IndentedJSON(http.StatusOK, &tokenPayload{ Access: tokenstr, Expires: exp - time.Now().Unix(), }) }
func SetRepo() gin.HandlerFunc { return func(c *gin.Context) { var ( owner = c.Param("owner") name = c.Param("name") ) user := User(c) repo, err := store.GetRepoOwnerName(c, owner, name) if err == nil { c.Set("repo", repo) c.Next() return } // if the user is not nil, check the remote system // to see if the repository actually exists. If yes, // we can prompt the user to add. if user != nil { remote := remote.FromContext(c) repo, err = remote.Repo(user, owner, name) if err != nil { log.Errorf("Cannot find remote repository %s/%s for user %s. %s", owner, name, user.Login, err) } else { log.Debugf("Found remote repository %s/%s for user %s", owner, name, user.Login) } } data := gin.H{ "User": user, "Repo": repo, } // if we found a repository, we should display a page // to the user allowing them to activate. if repo != nil && len(repo.FullName) != 0 { // we should probably move this code to a // separate route, but for now we need to // add a CSRF token. data["Csrf"], _ = token.New( token.CsrfToken, user.Login, ).Sign(user.Hash) c.HTML(http.StatusNotFound, "repo_activate.html", data) } else { c.HTML(http.StatusNotFound, "404.html", data) } c.Abort() } }
func DeleteRepo(c *gin.Context) { remote := remote.FromContext(c) repo := session.Repo(c) user := session.User(c) err := store.DeleteRepo(c, repo) if err != nil { c.AbortWithError(http.StatusInternalServerError, err) return } remote.Deactivate(user, repo, httputil.GetURL(c.Request)) c.Writer.WriteHeader(http.StatusOK) }
func GetRemoteRepos(c *gin.Context) { user := session.User(c) remote := remote.FromContext(c) reposv, ok := c.Get("repos") if ok { c.IndentedJSON(http.StatusOK, reposv) return } repos, err := remote.Repos(user) if err != nil { c.AbortWithStatus(http.StatusInternalServerError) return } c.Set("repos", repos) c.IndentedJSON(http.StatusOK, repos) }
func Refresh(c *gin.Context) { user := session.User(c) if user == nil { c.Next() return } // check if the remote includes the ability to // refresh the user token. remote_ := remote.FromContext(c) 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 := store.UpdateUser(c, 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) } else { log.Infof("refreshed access token for %s", user.Login) } } c.Next() }
func (u *updater) SetBuild(c context.Context, r *Task) error { err := store.UpdateBuild(c, r.Build) if err != nil { return err } err = remote.FromContext(c).Status(r.User, r.Repo, r.Build, fmt.Sprintf("%s/%s/%d", r.System.Link, r.Repo.FullName, r.Build.Number)) if err != nil { // log err } msg, err := json.Marshal(&payload{r.Build, r.Jobs}) if err != nil { return err } u.bus.send(&Event{ Name: r.Repo.FullName, Msg: msg, }) return nil }
func ShowIndex(c *gin.Context) { remote := remote.FromContext(c) user := session.User(c) if user == nil { c.Redirect(http.StatusSeeOther, "/login") return } var err error var repos []*model.RepoLite // get the repository list from the cache reposv, ok := c.Get("repos") if ok { repos = reposv.([]*model.RepoLite) } else { repos, err = remote.Repos(user) if err != nil { log.Errorf("Failure to get remote repositories for %s. %s.", user.Login, err) } else { c.Set("repos", repos) } } // for each repository in the remote system we get // the intersection of those repostiories in Drone repos_, err := store.GetRepoListOf(c, repos) if err != nil { log.Errorf("Failure to get repository list for %s. %s.", user.Login, err) } c.HTML(200, "repos.html", gin.H{ "User": user, "Repos": repos_, }) }
func PostRepo(c *gin.Context) { remote := remote.FromContext(c) user := session.User(c) owner := c.Param("owner") name := c.Param("name") if user == nil { c.AbortWithStatus(403) return } r, err := remote.Repo(user, owner, name) if err != nil { c.String(404, err.Error()) return } m, err := cache.GetPerms(c, user, owner, name) if err != nil { c.String(404, err.Error()) return } if !m.Admin { c.String(403, "Administrative access is required.") return } // error if the repository already exists _, err = store.GetRepoOwnerName(c, owner, name) if err == nil { c.String(409, "Repository already exists.") return } // set the repository owner to the // currently authenticated user. r.UserID = user.ID r.AllowPush = true r.AllowPull = true r.Timeout = 60 // 1 hour default build time r.Hash = base32.StdEncoding.EncodeToString( securecookie.GenerateRandomKey(32), ) // crates the jwt token used to verify the repository t := token.New(token.HookToken, r.FullName) sig, err := t.Sign(r.Hash) if err != nil { c.String(500, err.Error()) return } link := fmt.Sprintf( "%s/hook?access_token=%s", httputil.GetURL(c.Request), sig, ) // activate the repository before we make any // local changes to the database. err = remote.Activate(user, r, link) if err != nil { c.String(500, err.Error()) return } // persist the repository err = store.CreateRepo(c, r) if err != nil { c.String(500, err.Error()) return } c.JSON(200, r) }
func PostRepo(c *gin.Context) { remote := remote.FromContext(c) user := session.User(c) owner := c.Param("owner") name := c.Param("name") if user == nil { c.AbortWithStatus(403) return } r, err := remote.Repo(user, owner, name) if err != nil { c.String(404, err.Error()) return } m, err := remote.Perm(user, owner, name) if err != nil { c.String(404, err.Error()) return } if !m.Admin { c.String(403, "Administrative access is required.") return } // error if the repository already exists _, err = store.GetRepoOwnerName(c, owner, name) if err == nil { c.String(409, "Repository already exists.") return } // set the repository owner to the // currently authenticated user. r.UserID = user.ID r.AllowPush = true r.AllowPull = true r.Timeout = 60 // 1 hour default build time r.Hash = crypto.Rand() // crates the jwt token used to verify the repository t := token.New(token.HookToken, r.FullName) sig, err := t.Sign(r.Hash) if err != nil { c.String(500, err.Error()) return } link := fmt.Sprintf( "%s/hook?access_token=%s", httputil.GetURL(c.Request), sig, ) // generate an RSA key and add to the repo key, err := crypto.GeneratePrivateKey() if err != nil { c.String(500, err.Error()) return } keys := new(model.Key) keys.Public = string(crypto.MarshalPublicKey(&key.PublicKey)) keys.Private = string(crypto.MarshalPrivateKey(key)) // activate the repository before we make any // local changes to the database. err = remote.Activate(user, r, keys, link) if err != nil { c.String(500, err.Error()) return } // persist the repository err = store.CreateRepo(c, r) if err != nil { c.String(500, err.Error()) return } keys.RepoID = r.ID err = store.CreateKey(c, keys) if err != nil { c.String(500, err.Error()) return } c.JSON(200, r) }
func PostHook(c *gin.Context) { remote_ := remote.FromContext(c) tmprepo, build, err := remote_.Hook(c.Request) if err != nil { log.Errorf("failure to parse hook. %s", err) c.AbortWithError(400, err) return } if build == nil { c.Writer.WriteHeader(200) return } if tmprepo == nil { log.Errorf("failure to ascertain repo from hook.") c.Writer.WriteHeader(400) return } // skip the build if any case-insensitive combination of the words "skip" and "ci" // wrapped in square brackets appear in the commit message skipMatch := skipRe.FindString(build.Message) if len(skipMatch) > 0 { log.Infof("ignoring hook. %s found in %s", skipMatch, build.Commit) c.Writer.WriteHeader(204) return } repo, err := store.GetRepoOwnerName(c, tmprepo.Owner, tmprepo.Name) if err != nil { log.Errorf("failure to find repo %s/%s from hook. %s", tmprepo.Owner, tmprepo.Name, err) c.AbortWithError(404, err) return } // get the token and verify the hook is authorized parsed, err := token.ParseRequest(c.Request, func(t *token.Token) (string, error) { return repo.Hash, nil }) if err != nil { log.Errorf("failure to parse token from hook for %s. %s", repo.FullName, err) c.AbortWithError(400, err) return } if parsed.Text != repo.FullName { log.Errorf("failure to verify token from hook. Expected %s, got %s", repo.FullName, parsed.Text) c.AbortWithStatus(403) return } if repo.UserID == 0 { log.Warnf("ignoring hook. repo %s has no owner.", repo.FullName) c.Writer.WriteHeader(204) return } var skipped = true if (build.Event == model.EventPush && repo.AllowPush) || (build.Event == model.EventPull && repo.AllowPull) || (build.Event == model.EventDeploy && repo.AllowDeploy) || (build.Event == model.EventTag && repo.AllowTag) { skipped = false } if skipped { log.Infof("ignoring hook. repo %s is disabled for %s events.", repo.FullName, build.Event) c.Writer.WriteHeader(204) return } user, err := store.GetUser(c, repo.UserID) if err != nil { log.Errorf("failure to find repo owner %s. %s", repo.FullName, err) c.AbortWithError(500, err) return } // if there is no email address associated with the pull request, // we lookup the email address based on the authors github login. // // my initial hesitation with this code is that it has the ability // to expose your email address. At the same time, your email address // is already exposed in the public .git log. So while some people will // a small number of people will probably be upset by this, I'm not sure // it is actually that big of a deal. if len(build.Email) == 0 { author, err := store.GetUserLogin(c, build.Author) if err == nil { build.Email = author.Email } } // 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 { store.UpdateUser(c, user) } } // fetch the .drone.yml file from the database 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) return } axes, err := matrix.Parse(string(raw)) if err != nil { log.Errorf("failure to calculate matrix for %s. %s", repo.FullName, err) c.AbortWithError(400, err) return } if len(axes) == 0 { axes = append(axes, matrix.Axis{}) } 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) return } key, _ := store.GetKey(c, repo) // verify the branches can be built vs skipped yconfig, _ := yaml.Parse(string(raw)) var match = false for _, branch := range yconfig.Branches { if branch == build.Branch { match = true break } match, _ = filepath.Match(branch, build.Branch) if match { break } } if !match && len(yconfig.Branches) != 0 { log.Infof("ignoring hook. yaml file excludes repo and branch %s %s", repo.FullName, build.Branch) c.AbortWithStatus(200) return } // update some build fields build.Status = model.StatusPending build.RepoID = repo.ID // and use a transaction var jobs []*model.Job for num, axis := range axes { jobs = append(jobs, &model.Job{ BuildID: build.ID, Number: num + 1, Status: model.StatusPending, Environment: axis, }) } err = store.CreateBuild(c, build, jobs...) if err != nil { log.Errorf("failure to save commit for %s. %s", repo.FullName, err) c.AbortWithError(500, err) return } 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) if err != nil { log.Errorf("error setting commit status for %s/%d", repo.FullName, build.Number) } // get the previous build so taht we can send // on status change notifications last, _ := store.GetBuildLastBefore(c, repo, build.Branch, build.ID) engine_ := context.Engine(c) go engine_.Schedule(c.Copy(), &engine.Task{ User: user, Repo: repo, Build: build, BuildPrev: last, Jobs: jobs, Keys: key, Netrc: netrc, Config: string(raw), Secret: string(sec), System: &model.System{ Link: httputil.GetURL(c.Request), Plugins: strings.Split(os.Getenv("PLUGIN_FILTER"), " "), Globals: strings.Split(os.Getenv("PLUGIN_PARAMS"), " "), }, }) }
func PostHook(c *gin.Context) { remote_ := remote.FromContext(c) tmprepo, build, err := remote_.Hook(c.Request) if err != nil { log.Errorf("failure to parse hook. %s", err) c.AbortWithError(400, err) return } if build == nil { c.Writer.WriteHeader(200) return } if tmprepo == nil { log.Errorf("failure to ascertain repo from hook.") c.Writer.WriteHeader(400) return } // skip the build if any case-insensitive combination of the words "skip" and "ci" // wrapped in square brackets appear in the commit message skipMatch := skipRe.FindString(build.Message) if len(skipMatch) > 0 { log.Infof("ignoring hook. %s found in %s", skipMatch, build.Commit) c.Writer.WriteHeader(204) return } repo, err := store.GetRepoOwnerName(c, tmprepo.Owner, tmprepo.Name) if err != nil { log.Errorf("failure to find repo %s/%s from hook. %s", tmprepo.Owner, tmprepo.Name, err) c.AbortWithError(404, err) return } // get the token and verify the hook is authorized parsed, err := token.ParseRequest(c.Request, func(t *token.Token) (string, error) { return repo.Hash, nil }) if err != nil { log.Errorf("failure to parse token from hook for %s. %s", repo.FullName, err) c.AbortWithError(400, err) return } if parsed.Text != repo.FullName { log.Errorf("failure to verify token from hook. Expected %s, got %s", repo.FullName, parsed.Text) c.AbortWithStatus(403) return } if repo.UserID == 0 { log.Warnf("ignoring hook. repo %s has no owner.", repo.FullName) c.Writer.WriteHeader(204) return } var skipped = true if (build.Event == model.EventPush && repo.AllowPush) || (build.Event == model.EventPull && repo.AllowPull) || (build.Event == model.EventDeploy && repo.AllowDeploy) || (build.Event == model.EventTag && repo.AllowTag) { skipped = false } if skipped { log.Infof("ignoring hook. repo %s is disabled for %s events.", repo.FullName, build.Event) c.Writer.WriteHeader(204) return } user, err := store.GetUser(c, repo.UserID) if err != nil { log.Errorf("failure to find repo owner %s. %s", repo.FullName, err) c.AbortWithError(500, err) return } // if there is no email address associated with the pull request, // we lookup the email address based on the authors github login. // // my initial hesitation with this code is that it has the ability // to expose your email address. At the same time, your email address // is already exposed in the public .git log. So while some people will // a small number of people will probably be upset by this, I'm not sure // it is actually that big of a deal. if len(build.Email) == 0 { author, err := store.GetUserLogin(c, build.Author) if err == nil { build.Email = author.Email } } // 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 { store.UpdateUser(c, user) } } // fetch the build file from the database config := ToConfig(c) raw, err := remote_.File(user, repo, build, config.Yaml) if err != nil { log.Errorf("failure to get build config for %s. %s", repo.FullName, err) c.AbortWithError(404, err) return } sec, err := remote_.File(user, repo, build, config.Shasum) if err != nil { log.Debugf("cannot find build secrets for %s. %s", repo.FullName, err) // NOTE we don't exit on failure. The sec file is optional } axes, err := yaml.ParseMatrix(raw) if err != nil { c.String(500, "Failed to parse yaml file or calculate matrix. %s", err) return } if len(axes) == 0 { axes = append(axes, yaml.Axis{}) } netrc, err := remote_.Netrc(user, repo) if err != nil { c.String(500, "Failed to generate netrc file. %s", err) return } // verify the branches can be built vs skipped branches := yaml.ParseBranch(raw) if !branches.Match(build.Branch) && build.Event != model.EventTag && build.Event != model.EventDeploy { c.String(200, "Branch does not match restrictions defined in yaml") return } signature, err := jose.ParseSigned(string(sec)) if err != nil { log.Debugf("cannot parse .drone.yml.sig file. %s", err) } else if len(sec) == 0 { log.Debugf("cannot parse .drone.yml.sig file. empty file") } else { build.Signed = true output, err := signature.Verify([]byte(repo.Hash)) if err != nil { log.Debugf("cannot verify .drone.yml.sig file. %s", err) } else if string(output) != string(raw) { log.Debugf("cannot verify .drone.yml.sig file. no match") } else { build.Verified = true } } // update some build fields build.Status = model.StatusPending build.RepoID = repo.ID // and use a transaction var jobs []*model.Job for num, axis := range axes { jobs = append(jobs, &model.Job{ BuildID: build.ID, Number: num + 1, Status: model.StatusPending, Environment: axis, }) } err = store.CreateBuild(c, build, jobs...) if err != nil { log.Errorf("failure to save commit for %s. %s", repo.FullName, err) c.AbortWithError(500, err) return } 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) if err != nil { log.Errorf("error setting commit status for %s/%d", repo.FullName, build.Number) } // get the previous build so that we can send // on status change notifications last, _ := store.GetBuildLastBefore(c, repo, build.Branch, build.ID) secs, err := store.GetSecretList(c, repo) if err != nil { log.Errorf("Error getting secrets for %s#%d. %s", repo.FullName, build.Number, err) } bus.Publish(c, bus.NewBuildEvent(bus.Enqueued, repo, build)) for _, job := range jobs { queue.Publish(c, &queue.Work{ Signed: build.Signed, Verified: build.Verified, User: user, Repo: repo, Build: build, BuildLast: last, Job: job, Netrc: netrc, Yaml: string(raw), Secrets: secs, System: &model.System{Link: httputil.GetURL(c.Request)}, }) } }
func PostBuild(c *gin.Context) { remote_ := remote.FromContext(c) repo := session.Repo(c) fork := c.DefaultQuery("fork", "false") num, err := strconv.Atoi(c.Param("number")) if err != nil { c.AbortWithError(http.StatusBadRequest, err) return } user, err := store.GetUser(c, repo.UserID) if err != nil { log.Errorf("failure to find repo owner %s. %s", repo.FullName, err) c.AbortWithError(500, err) return } build, err := store.GetBuildNumber(c, repo, num) if err != nil { log.Errorf("failure to get build %d. %s", num, err) c.AbortWithError(404, err) 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 { store.UpdateUser(c, user) } } // fetch the .drone.yml file from the database config := ToConfig(c) raw, err := remote_.File(user, repo, build, config.Yaml) if err != nil { log.Errorf("failure to get build config for %s. %s", repo.FullName, err) c.AbortWithError(404, err) return } // Fetch secrets file but don't exit on error as it's optional sec, err := remote_.File(user, repo, build, config.Shasum) if err != nil { log.Debugf("cannot find build secrets for %s. %s", repo.FullName, err) } 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) return } jobs, err := store.GetJobList(c, build) if err != nil { log.Errorf("failure to get build %d jobs. %s", build.Number, err) c.AbortWithError(404, err) return } // must not restart a running build if build.Status == model.StatusPending || build.Status == model.StatusRunning { c.String(409, "Cannot re-start a started build") return } // forking the build creates a duplicate of the build // and then executes. This retains prior build history. if forkit, _ := strconv.ParseBool(fork); forkit { build.ID = 0 build.Number = 0 for _, job := range jobs { job.ID = 0 job.NodeID = 0 } err := store.CreateBuild(c, build, jobs...) if err != nil { c.String(500, err.Error()) return } event := c.DefaultQuery("event", build.Event) if event == model.EventPush || event == model.EventPull || event == model.EventTag || event == model.EventDeploy { build.Event = event } build.Deploy = c.DefaultQuery("deploy_to", build.Deploy) } // todo move this to database tier // and wrap inside a transaction build.Status = model.StatusPending build.Started = 0 build.Finished = 0 build.Enqueued = time.Now().UTC().Unix() for _, job := range jobs { job.Status = model.StatusPending job.Started = 0 job.Finished = 0 job.ExitCode = 0 job.NodeID = 0 job.Enqueued = build.Enqueued store.UpdateJob(c, job) } err = store.UpdateBuild(c, build) if err != nil { c.AbortWithStatus(500) return } c.JSON(202, build) // get the previous build so that we can send // on status change notifications last, _ := store.GetBuildLastBefore(c, repo, build.Branch, build.ID) secs, err := store.GetSecretList(c, repo) if err != nil { log.Errorf("Error getting secrets for %s#%d. %s", repo.FullName, build.Number, err) } var signed bool var verified bool signature, err := jose.ParseSigned(string(sec)) if err != nil { log.Debugf("cannot parse .drone.yml.sig file. %s", err) } else if len(sec) == 0 { log.Debugf("cannot parse .drone.yml.sig file. empty file") } else { signed = true output, err := signature.Verify([]byte(repo.Hash)) if err != nil { log.Debugf("cannot verify .drone.yml.sig file. %s", err) } else if string(output) != string(raw) { log.Debugf("cannot verify .drone.yml.sig file. no match. %q <> %q", string(output), string(raw)) } else { verified = true } } log.Debugf(".drone.yml is signed=%v and verified=%v", signed, verified) bus.Publish(c, bus.NewBuildEvent(bus.Enqueued, repo, build)) for _, job := range jobs { queue.Publish(c, &queue.Work{ Signed: signed, Verified: verified, User: user, Repo: repo, Build: build, BuildLast: last, Job: job, Netrc: netrc, Yaml: string(raw), Secrets: secs, System: &model.System{Link: httputil.GetURL(c.Request)}, }) } }
func GetLogin(c *gin.Context) { remote := remote.FromContext(c) // when dealing with redirects we may need // to adjust the content type. I cannot, however, // remember why, so need to revisit this line. c.Writer.Header().Del("Content-Type") tmpuser, open, err := remote.Login(c.Writer, c.Request) if err != nil { log.Errorf("cannot authenticate user. %s", err) c.Redirect(303, "/login?error=oauth_error") return } // this will happen when the user is redirected by // the remote provide as part of the oauth dance. if tmpuser == nil { return } // get the user from the database u, err := store.GetUserLogin(c, tmpuser.Login) if err != nil { count, err := store.CountUsers(c) if err != nil { log.Errorf("cannot register %s. %s", tmpuser.Login, err) c.Redirect(303, "/login?error=internal_error") return } // if self-registration is disabled we should // return a notAuthorized error. the only exception // is if no users exist yet in the system we'll proceed. if !open && count != 0 { log.Errorf("cannot register %s. registration closed", tmpuser.Login) c.Redirect(303, "/login?error=access_denied") return } // create the user account u = &model.User{} u.Login = tmpuser.Login u.Token = tmpuser.Token u.Secret = tmpuser.Secret u.Email = tmpuser.Email u.Avatar = tmpuser.Avatar u.Hash = crypto.Rand() // insert the user into the database if err := store.CreateUser(c, u); err != nil { log.Errorf("cannot insert %s. %s", u.Login, err) c.Redirect(303, "/login?error=internal_error") return } // if this is the first user, they // should be an admin. if count == 0 { u.Admin = true } } // update the user meta data and authorization // data and cache in the datastore. u.Token = tmpuser.Token u.Secret = tmpuser.Secret u.Email = tmpuser.Email u.Avatar = tmpuser.Avatar if err := store.UpdateUser(c, u); err != nil { log.Errorf("cannot update %s. %s", u.Login, err) c.Redirect(303, "/login?error=internal_error") return } exp := time.Now().Add(time.Hour * 72).Unix() token := token.New(token.SessToken, u.Login) tokenstr, err := token.SignExpires(u.Hash, exp) if err != nil { log.Errorf("cannot create token for %s. %s", u.Login, err) c.Redirect(303, "/login?error=internal_error") return } httputil.SetCookie(c.Writer, c.Request, "user_sess", tokenstr) redirect := httputil.GetCookie(c.Request, "user_last") if len(redirect) == 0 { redirect = "/" } c.Redirect(303, redirect) }
func SetPerm() gin.HandlerFunc { PUBLIC_MODE := os.Getenv("PUBLIC_MODE") return func(c *gin.Context) { user := User(c) repo := Repo(c) perm := &model.Perm{} if user != nil { // attempt to get the permissions from a local cache // just to avoid excess API calls to GitHub val, ok := c.Get("perm") if ok { c.Next() log.Debugf("%s using cached %+v permission to %s", user.Login, val, repo.FullName) return } } switch { // if the user is not authenticated, and the // repository is private, the user has NO permission // to view the repository. case user == nil && repo.IsPrivate == true: perm.Pull = false perm.Push = false perm.Admin = false // if the user is not authenticated, but the repository // is public, the user has pull-rights only. case user == nil && repo.IsPrivate == false: perm.Pull = true perm.Push = false perm.Admin = false case user.Admin: perm.Pull = true perm.Push = true perm.Admin = true // otherwise if the user is authenticated we should // check the remote system to get the users permissiosn. default: var err error perm, err = remote.FromContext(c).Perm(user, repo.Owner, repo.Name) if err != nil { perm.Pull = false perm.Push = false perm.Admin = false // debug log.Errorf("Error fetching permission for %s %s", user.Login, repo.FullName) } // if we couldn't fetch permissions, but the repository // is public, we should grant the user pull access. if err != nil && repo.IsPrivate == false { perm.Pull = true } } // all build logs are visible in public mode if PUBLIC_MODE != "" { perm.Pull = true } if user != nil { log.Debugf("%s granted %+v permission to %s", user.Login, perm, repo.FullName) } else { log.Debugf("Guest granted %+v to %s", perm, repo.FullName) } c.Set("perm", perm) c.Next() } }
func PostBuild(c *gin.Context) { remote_ := remote.FromContext(c) repo := session.Repo(c) num, err := strconv.Atoi(c.Param("number")) if err != nil { c.AbortWithError(http.StatusBadRequest, err) return } user, err := store.GetUser(c, repo.UserID) if err != nil { log.Errorf("failure to find repo owner %s. %s", repo.FullName, err) c.AbortWithError(500, err) return } build, err := store.GetBuildNumber(c, repo, num) if err != nil { log.Errorf("failure to get build %d. %s", num, err) c.AbortWithError(404, err) 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 { store.UpdateUser(c, user) } } // fetch the .drone.yml file from the database 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) return } key, _ := store.GetKey(c, 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) return } jobs, err := store.GetJobList(c, build) if err != nil { log.Errorf("failure to get build %d jobs. %s", build.Number, err) c.AbortWithError(404, err) return } // must not restart a running build if build.Status == model.StatusPending || build.Status == model.StatusRunning { c.AbortWithStatus(409) return } // todo move this to database tier // and wrap inside a transaction build.Status = model.StatusPending build.Started = 0 build.Finished = 0 build.Enqueued = time.Now().UTC().Unix() for _, job := range jobs { job.Status = model.StatusPending job.Started = 0 job.Finished = 0 job.ExitCode = 0 job.Enqueued = build.Enqueued store.UpdateJob(c, job) } err = store.UpdateBuild(c, build) if err != nil { c.AbortWithStatus(500) return } c.JSON(202, build) // get the previous build so taht we can send // on status change notifications last, _ := store.GetBuildLastBefore(c, repo, build.Branch, build.ID) engine_ := context.Engine(c) go engine_.Schedule(c.Copy(), &engine.Task{ User: user, Repo: repo, Build: build, BuildPrev: last, Jobs: jobs, Keys: key, Netrc: netrc, Config: string(raw), Secret: string(sec), System: &model.System{ Link: httputil.GetURL(c.Request), Plugins: strings.Split(os.Getenv("PLUGIN_FILTER"), " "), Globals: strings.Split(os.Getenv("PLUGIN_PARAMS"), " "), }, }) }
func PostBuild(c *gin.Context) { remote_ := remote.FromContext(c) repo := session.Repo(c) fork := c.DefaultQuery("fork", "false") num, err := strconv.Atoi(c.Param("number")) if err != nil { c.AbortWithError(http.StatusBadRequest, err) return } user, err := store.GetUser(c, repo.UserID) if err != nil { log.Errorf("failure to find repo owner %s. %s", repo.FullName, err) c.AbortWithError(500, err) return } build, err := store.GetBuildNumber(c, repo, num) if err != nil { log.Errorf("failure to get build %d. %s", num, err) c.AbortWithError(404, err) 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 { store.UpdateUser(c, user) } } // fetch the .drone.yml file from the database config := ToConfig(c) raw, err := remote_.File(user, repo, build, config.Yaml) if err != nil { log.Errorf("failure to get build config for %s. %s", repo.FullName, err) c.AbortWithError(404, err) return } // Fetch secrets file but don't exit on error as it's optional sec, err := remote_.File(user, repo, build, config.Shasum) if err != nil { log.Debugf("cannot find build secrets for %s. %s", repo.FullName, err) } 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) return } jobs, err := store.GetJobList(c, build) if err != nil { log.Errorf("failure to get build %d jobs. %s", build.Number, err) c.AbortWithError(404, err) return } // must not restart a running build if build.Status == model.StatusPending || build.Status == model.StatusRunning { c.String(409, "Cannot re-start a started build") return } // forking the build creates a duplicate of the build // and then executes. This retains prior build history. if forkit, _ := strconv.ParseBool(fork); forkit { build.ID = 0 build.Number = 0 build.Parent = num for _, job := range jobs { job.ID = 0 job.NodeID = 0 } err := store.CreateBuild(c, build, jobs...) if err != nil { c.String(500, err.Error()) return } event := c.DefaultQuery("event", build.Event) if event == model.EventPush || event == model.EventPull || event == model.EventTag || event == model.EventDeploy { build.Event = event } build.Deploy = c.DefaultQuery("deploy_to", build.Deploy) } // Read query string parameters into buildParams, exclude reserved params var buildParams = map[string]string{} for key, val := range c.Request.URL.Query() { switch key { case "fork", "event", "deploy_to": default: // We only accept string literals, because build parameters will be // injected as environment variables buildParams[key] = val[0] } } // todo move this to database tier // and wrap inside a transaction build.Status = model.StatusPending build.Started = 0 build.Finished = 0 build.Enqueued = time.Now().UTC().Unix() for _, job := range jobs { for k, v := range buildParams { job.Environment[k] = v } job.Error = "" job.Status = model.StatusPending job.Started = 0 job.Finished = 0 job.ExitCode = 0 job.NodeID = 0 job.Enqueued = build.Enqueued store.UpdateJob(c, job) } err = store.UpdateBuild(c, build) if err != nil { c.AbortWithStatus(500) return } c.JSON(202, build) // get the previous build so that we can send // on status change notifications last, _ := store.GetBuildLastBefore(c, repo, build.Branch, build.ID) secs, err := store.GetMergedSecretList(c, repo) if err != nil { log.Debugf("Error getting secrets for %s#%d. %s", repo.FullName, build.Number, err) } var signed bool var verified bool signature, err := jose.ParseSigned(string(sec)) if err != nil { log.Debugf("cannot parse .drone.yml.sig file. %s", err) } else if len(sec) == 0 { log.Debugf("cannot parse .drone.yml.sig file. empty file") } else { signed = true output, err := signature.Verify([]byte(repo.Hash)) if err != nil { log.Debugf("cannot verify .drone.yml.sig file. %s", err) } else if string(output) != string(raw) { log.Debugf("cannot verify .drone.yml.sig file. no match. %q <> %q", string(output), string(raw)) } else { verified = true } } log.Debugf(".drone.yml is signed=%v and verified=%v", signed, verified) client := stomp.MustFromContext(c) client.SendJSON("/topic/events", model.Event{ Type: model.Enqueued, Repo: *repo, Build: *build, }, stomp.WithHeader("repo", repo.FullName), stomp.WithHeader("private", strconv.FormatBool(repo.IsPrivate)), ) for _, job := range jobs { broker, _ := stomp.FromContext(c) broker.SendJSON("/queue/pending", &model.Work{ Signed: signed, Verified: verified, User: user, Repo: repo, Build: build, BuildLast: last, Job: job, Netrc: netrc, Yaml: string(raw), Secrets: secs, System: &model.System{Link: httputil.GetURL(c.Request)}, }, stomp.WithHeader( "platform", yaml.ParsePlatformDefault(raw, "linux/amd64"), ), stomp.WithHeaders( yaml.ParseLabel(raw), ), ) } }