diff --git a/db.go b/db.go index 11e005c..9631502 100644 --- a/db.go +++ b/db.go @@ -234,6 +234,7 @@ CREATE TABLE IF NOT EXISTS user_work_repositories( id_user INTEGER NOT NULL, id_work INTEGER NOT NULL, uri VARCHAR(255) NOT NULL, + secret BLOB NOT NULL, last_check TIMESTAMP NULL DEFAULT NULL, droneref VARCHAR(255) NOT NULL, FOREIGN KEY(id_user) REFERENCES users(id_user), diff --git a/gitlab.go b/gitlab.go index 4d97c56..54f3db6 100644 --- a/gitlab.go +++ b/gitlab.go @@ -171,6 +171,7 @@ type GitLabRepository struct { Path string PathWithNamespace string `json:"path_with_namespace"` DefaultBranch string `json:"default_branch"` + URL string `json:"url,omitempty"` SshUrlToRepo string `json:"ssh_url_to_repo"` HttpUrlToRepo string `json:"http_url_to_repo"` AvatarURL string `json:"avatar_url"` diff --git a/repositories.go b/repositories.go index 6adfd14..08e78c8 100644 --- a/repositories.go +++ b/repositories.go @@ -1,6 +1,8 @@ package main import ( + "crypto/rand" + "encoding/base64" "flag" "fmt" "log" @@ -169,34 +171,12 @@ func declareAPIAuthRepositoriesRoutes(router *gin.RouterGroup) { now := time.Now() - if !work.Shown || work.Corrected || work.StartAvailability.After(now) || work.EndAvailability.Add(time.Hour).Before(now) { - c.AbortWithStatusJSON(http.StatusForbidden, gin.H{"errmsg": "The submission is closed."}) - return - } - if repo.LastCheck != nil && !repo.LastCheck.Before(now.Add(-5*time.Minute)) { c.AbortWithStatusJSON(http.StatusPaymentRequired, gin.H{"errmsg": "Please wait between two pulls."}) return } - client := drone.NewClient(droneEndpoint, droneConfig) - result, err := client.BuildCreate("srs", "atsebay.t-worker", "", "master", map[string]string{ - "REPO_URL": repo.URI, - "REPO_TAG": work.Tag, - "LOGIN": u.Login, - "DEST": fmt.Sprintf("%d", work.Id), - }) - if err != nil { - log.Println("Unable to communicate with Drone:", err.Error()) - c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "Unable to communication with the extraction service."}) - return - } - - repo.DroneRef = fmt.Sprintf("%s/%s/%d", "srs", "atsebay.t-worker", result.Number) - repo.LastCheck = &now - repo.Update() - - c.JSON(http.StatusOK, repo) + TriggerTagUpdate(c, work, repo, u, nil) }) repositoriesRoutes.GET("/state", func(c *gin.Context) { @@ -254,20 +234,77 @@ func declareAPIAuthRepositoriesRoutes(router *gin.RouterGroup) { }) } +type GitLabWebhook struct { + EventName string `json:"event_name"` + ObjectKind string `json:"object_kind"` + Ref string + Repository GitLabRepository +} + func declareCallbacksRoutes(router *gin.RouterGroup) { router.POST("/callbacks/trigger.json", func(c *gin.Context) { - log.Println("Received trigger") - log.Println("X-Gitlab-Token", c.Request.Header.Get("X-Gitlab-Token")) + // Check event type + if c.Request.Header.Get("X-Gitlab-Event") != "Tag Push Hook" { + c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": "This trigger is limited to Tag Push event. Please edit your trigger."}) + return + } - tmp := map[string]interface{}{} - if err := c.ShouldBindJSON(&tmp); err != nil { + hook := GitLabWebhook{} + if err := c.ShouldBindJSON(&hook); err != nil { c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": err.Error()}) return } - log.Println("Content", tmp) + // Check token + repos, err := getRepositoriesByURI(hook.Repository.URL) + if err != nil { + log.Println("Unable to getRepositoriesByURI:", err.Error()) + c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "Unable to retrieve repositories."}) + return + } - c.JSON(http.StatusOK, true) + var repo *Repository + for _, r := range repos { + log.Println("Received trigger") + if len(r.Secret) == 0 || base64.StdEncoding.EncodeToString(r.Secret) == c.Request.Header.Get("X-Gitlab-Token") { + repo = r + break + } + } + + if repo == nil { + c.AbortWithStatusJSON(http.StatusNotFound, gin.H{"errmsg": "There are no repositories matching this URL and secret. Please check the Gitlab Secret and retry pushing your tag."}) + return + } + + work, err := getWork(int(repo.IdWork)) + if err != nil { + log.Println("Unable to getWork:", err.Error()) + c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "Unable to find related work."}) + return + + } + + user, err := getUser(int(repo.IdUser)) + if err != nil { + log.Println("Unable to getUser:", err.Error()) + c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "Unable to find related user."}) + return + + } + + tmp := strings.SplitN(hook.Ref, "/", 3) + if len(tmp) != 3 { + TriggerTagUpdate(c, work, repo, user, nil) + return + } + + if !strings.HasPrefix(tmp[2], work.Tag) { + c.AbortWithStatusJSON(http.StatusOK, gin.H{"errmsg": fmt.Sprintf("Ignore ref %q has it doesn't start with %s. Check submission instructions if this is not expected.", tmp[2], work.Tag)}) + return + } + + TriggerTagUpdate(c, work, repo, user, &tmp[2]) }) } @@ -299,24 +336,80 @@ func repositoryHandler(c *gin.Context) { } } +func TriggerTagUpdate(c *gin.Context, work *Work, repo *Repository, u *User, tag *string) { + now := time.Now() + + if !work.Shown || work.Corrected || work.StartAvailability.After(now) || work.EndAvailability.Add(time.Hour).Before(now) { + c.AbortWithStatusJSON(http.StatusForbidden, gin.H{"errmsg": "The submission is closed."}) + return + } + + repo_tag := work.Tag + if tag != nil { + repo_tag = *tag + } + + client := drone.NewClient(droneEndpoint, droneConfig) + result, err := client.BuildCreate("srs", "atsebay.t-worker", "", "master", map[string]string{ + "REPO_URL": repo.URI, + "REPO_TAG": repo_tag, + "LOGIN": u.Login, + "DEST": fmt.Sprintf("%d", work.Id), + }) + if err != nil { + log.Println("Unable to communicate with Drone:", err.Error()) + c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "Unable to communication with the extraction service."}) + return + } + + repo.DroneRef = fmt.Sprintf("%s/%s/%d", "srs", "atsebay.t-worker", result.Number) + repo.LastCheck = &now + repo.Update() + + repo.Secret = []byte{} + c.JSON(http.StatusOK, repo) +} + type Repository struct { Id int64 `json:"id"` IdUser int64 `json:"id_user"` IdWork int64 `json:"id_work"` URI string `json:"uri"` + Secret []byte `json:"secret,omitempty"` LastCheck *time.Time `json:"last_check"` DroneRef string `json:"drone_ref,omitempty"` } func (u *User) GetRepositories() (repositories []*Repository, err error) { - if rows, errr := DBQuery("SELECT id_repository, id_user, id_work, uri, last_check, droneref FROM user_work_repositories WHERE id_user=?", u.Id); errr != nil { + if rows, errr := DBQuery("SELECT id_repository, id_user, id_work, uri, secret, last_check, droneref FROM user_work_repositories WHERE id_user=?", u.Id); errr != nil { return nil, errr } else { defer rows.Close() for rows.Next() { var repo Repository - if err = rows.Scan(&repo.Id, &repo.IdUser, &repo.IdWork, &repo.URI, &repo.LastCheck, &repo.DroneRef); err != nil { + if err = rows.Scan(&repo.Id, &repo.IdUser, &repo.IdWork, &repo.URI, &repo.Secret, &repo.LastCheck, &repo.DroneRef); err != nil { + return + } + repositories = append(repositories, &repo) + } + if err = rows.Err(); err != nil { + return + } + + return + } +} + +func getRepositoriesByURI(uri string) (repositories []*Repository, err error) { + if rows, errr := DBQuery("SELECT id_repository, id_user, id_work, uri, secret, last_check, droneref FROM user_work_repositories WHERE uri=?", uri); errr != nil { + return nil, errr + } else { + defer rows.Close() + + for rows.Next() { + var repo Repository + if err = rows.Scan(&repo.Id, &repo.IdUser, &repo.IdWork, &repo.URI, &repo.Secret, &repo.LastCheck, &repo.DroneRef); err != nil { return } repositories = append(repositories, &repo) @@ -331,28 +424,34 @@ func (u *User) GetRepositories() (repositories []*Repository, err error) { func getRepository(id int) (r *Repository, err error) { r = new(Repository) - err = DBQueryRow("SELECT id_repository, id_user, id_work, uri, last_check, droneref FROM user_work_repositories WHERE id_repository=?", id).Scan(&r.Id, &r.IdUser, &r.IdWork, &r.URI, &r.LastCheck, &r.DroneRef) + err = DBQueryRow("SELECT id_repository, id_user, id_work, uri, secret, last_check, droneref FROM user_work_repositories WHERE id_repository=?", id).Scan(&r.Id, &r.IdUser, &r.IdWork, &r.URI, &r.Secret, &r.LastCheck, &r.DroneRef) return } func (u *User) getRepository(id int) (r *Repository, err error) { r = new(Repository) - err = DBQueryRow("SELECT id_repository, id_user, id_work, uri, last_check, droneref FROM user_work_repositories WHERE id_repository=? AND id_user=?", id, u.Id).Scan(&r.Id, &r.IdUser, &r.IdWork, &r.URI, &r.LastCheck, &r.DroneRef) + err = DBQueryRow("SELECT id_repository, id_user, id_work, uri, secret, last_check, droneref FROM user_work_repositories WHERE id_repository=? AND id_user=?", id, u.Id).Scan(&r.Id, &r.IdUser, &r.IdWork, &r.URI, &r.Secret, &r.LastCheck, &r.DroneRef) return } func (u *User) NewRepository(w *Work, uri string) (*Repository, error) { - if res, err := DBExec("INSERT INTO user_work_repositories (id_user, id_work, uri, droneref) VALUES (?, ?, ?, ?)", u.Id, w.Id, uri, ""); err != nil { + secret := make([]byte, 24) + _, err := rand.Read(secret) + if err != nil { + return nil, err + } + + if res, err := DBExec("INSERT INTO user_work_repositories (id_user, id_work, uri, secret, droneref) VALUES (?, ?, ?, ?, ?)", u.Id, w.Id, uri, secret, ""); err != nil { return nil, err } else if rid, err := res.LastInsertId(); err != nil { return nil, err } else { - return &Repository{rid, u.Id, w.Id, uri, nil, ""}, nil + return &Repository{rid, u.Id, w.Id, uri, secret, nil, ""}, nil } } func (r *Repository) Update() (*Repository, error) { - if _, err := DBExec("UPDATE user_work_repositories SET id_user = ?, id_work = ?, uri = ?, last_check = ?, droneref = ? WHERE id_repository = ?", r.IdUser, r.IdWork, r.URI, r.LastCheck, r.DroneRef, r.Id); err != nil { + if _, err := DBExec("UPDATE user_work_repositories SET id_user = ?, id_work = ?, uri = ?, secret = ?, last_check = ?, droneref = ? WHERE id_repository = ?", r.IdUser, r.IdWork, r.URI, r.Secret, r.LastCheck, r.DroneRef, r.Id); err != nil { return nil, err } else { return r, err diff --git a/ui/src/components/WorkRepository.svelte b/ui/src/components/WorkRepository.svelte index 2a2361c..682f3a6 100644 --- a/ui/src/components/WorkRepository.svelte +++ b/ui/src/components/WorkRepository.svelte @@ -84,7 +84,23 @@
- Dépôt lié : {repo.uri}
+
+ +
+ +
+
+
+ +
+
+ + +
+
+
Dernière récupération : {#if repo.last_check}{:else}-{/if} {#if repo_pull_state} {#await repo_pull_state} diff --git a/ui/src/lib/repositories.js b/ui/src/lib/repositories.js index b909887..e69ae1a 100644 --- a/ui/src/lib/repositories.js +++ b/ui/src/lib/repositories.js @@ -5,11 +5,12 @@ export class WorkRepository { } } - update({ id, id_user, id_work, uri, last_check }) { + update({ id, id_user, id_work, uri, secret, last_check }) { this.id = id; this.id_user = id_user; this.id_work = id_work; this.uri = uri; + this.secret = secret; this.last_check = last_check; } diff --git a/ui/src/routes/works/[wid]/index.svelte b/ui/src/routes/works/[wid]/index.svelte index 2972052..3fde8e5 100644 --- a/ui/src/routes/works/[wid]/index.svelte +++ b/ui/src/routes/works/[wid]/index.svelte @@ -146,7 +146,7 @@
  • être dans l'espace de nom de votre utilisateur (à la fin de la liste des namespaces),
  • avoir la visibilité « Privé »,
  • avoir invité nemunaire avec le rôle Reporter une fois le dépôt créé,
  • -
  • configuré un webhook pointant sur https://lessons.nemunai.re/api/callbacks/trigger.json
  • +
  • avoir configuré un webhook Tag push events pointant sur https://lessons.nemunai.re/api/callbacks/trigger.json avec le secret donné.
  • {#if w.tag}