func SetUser() gin.HandlerFunc { return func(c *gin.Context) { var user *model.User t, err := token.ParseRequest(c.Request, func(t *token.Token) (string, error) { var err error user, err = store.GetUserLogin(c, t.Text) return user.Hash, err }) if err == nil { confv := c.MustGet("config") if conf, ok := confv.(*model.Config); ok { user.Admin = conf.IsAdmin(user) } c.Set("user", user) // if this is a session token (ie not the API token) // this means the user is accessing with a web browser, // so we should implement CSRF protection measures. if t.Kind == token.SessToken { err = token.CheckCsrf(c.Request, func(t *token.Token) (string, error) { return user.Hash, nil }) // if csrf token validation fails, exit immediately // with a not authorized error. if err != nil { c.AbortWithStatus(http.StatusUnauthorized) return } } } c.Next() } }
func GetLoginToken(c *gin.Context) { in := &tokenPayload{} err := c.Bind(in) if err != nil { c.AbortWithError(http.StatusBadRequest, err) return } login, err := remote.Auth(c, 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.JSON(http.StatusOK, &tokenPayload{ Access: tokenstr, Expires: exp - time.Now().Unix(), }) }
func PatchUser(c *gin.Context) { me := session.User(c) in := &model.User{} err := c.Bind(in) if err != nil { c.AbortWithStatus(http.StatusBadRequest) return } user, err := store.GetUserLogin(c, c.Param("login")) if err != nil { c.AbortWithStatus(http.StatusNotFound) return } user.Admin = in.Admin user.Active = in.Active // cannot update self if me.ID == user.ID { c.AbortWithStatus(http.StatusForbidden) return } err = store.UpdateUser(c, user) if err != nil { c.AbortWithStatus(http.StatusConflict) return } c.IndentedJSON(http.StatusOK, user) }
func GetUser(c *gin.Context) { user, err := store.GetUserLogin(c, c.Param("login")) if err != nil { c.String(404, "Cannot find user. %s", err) return } c.JSON(200, user) }
func GetUser(c *gin.Context) { user, err := store.GetUserLogin(c, c.Param("login")) if err != nil { c.AbortWithStatus(http.StatusNotFound) return } c.IndentedJSON(http.StatusOK, user) }
func DeleteUser(c *gin.Context) { user, err := store.GetUserLogin(c, c.Param("login")) if err != nil { c.String(404, "Cannot find user. %s", err) return } if err = store.DeleteUser(c, user); err != nil { c.String(500, "Error deleting user. %s", err) return } c.String(200, "") }
func PatchUser(c *gin.Context) { in := &model.User{} err := c.Bind(in) if err != nil { c.AbortWithStatus(http.StatusBadRequest) return } user, err := store.GetUserLogin(c, c.Param("login")) if err != nil { c.AbortWithStatus(http.StatusNotFound) return } user.Active = in.Active err = store.UpdateUser(c, user) if err != nil { c.AbortWithStatus(http.StatusConflict) return } c.JSON(http.StatusOK, user) }
func DeleteUser(c *gin.Context) { me := session.User(c) user, err := store.GetUserLogin(c, c.Param("login")) if err != nil { c.AbortWithStatus(http.StatusNotFound) return } // cannot delete self if me.ID == user.ID { c.AbortWithStatus(http.StatusForbidden) return } err = store.DeleteUser(c, user) if err != nil { c.AbortWithStatus(http.StatusInternalServerError) return } c.Writer.WriteHeader(http.StatusNoContent) }
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 GetLogin(c *gin.Context) { // 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, err := remote.Login(c, c.Writer, c.Request) if err != nil { logrus.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 provider as // part of the authorization workflow. if tmpuser == nil { return } config := ToConfig(c) // get the user from the database u, err := store.GetUserLogin(c, tmpuser.Login) if err != nil { // if self-registration is disabled we should return a not authorized error if !config.Open && !config.IsAdmin(tmpuser) { logrus.Errorf("cannot register %s. registration closed", tmpuser.Login) c.Redirect(303, "/login?error=access_denied") return } // if self-registration is enabled for whitelisted organizations we need to // check the user's organization membership. if len(config.Orgs) != 0 { teams, terr := remote.Teams(c, tmpuser) if terr != nil || config.IsMember(teams) == false { logrus.Errorf("cannot verify team membership for %s.", u.Login) c.Redirect(303, "/login?error=access_denied") return } } // create the user account u = &model.User{ Login: tmpuser.Login, Token: tmpuser.Token, Secret: tmpuser.Secret, Email: tmpuser.Email, Avatar: tmpuser.Avatar, Hash: base32.StdEncoding.EncodeToString( securecookie.GenerateRandomKey(32), ), } // insert the user into the database if err := store.CreateUser(c, u); err != nil { logrus.Errorf("cannot insert %s. %s", u.Login, err) c.Redirect(303, "/login?error=internal_error") return } } // update the user meta data and authorization data. u.Token = tmpuser.Token u.Secret = tmpuser.Secret u.Email = tmpuser.Email u.Avatar = tmpuser.Avatar // if self-registration is enabled for whitelisted organizations we need to // check the user's organization membership. if len(config.Orgs) != 0 { teams, terr := remote.Teams(c, u) if terr != nil || config.IsMember(teams) == false { logrus.Errorf("cannot verify team membership for %s.", u.Login) c.Redirect(303, "/login?error=access_denied") return } } if err := store.UpdateUser(c, u); err != nil { logrus.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 { logrus.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) c.Redirect(303, "/") }
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 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) }