Introduce user repositories

This commit is contained in:
nemunaire 2022-09-04 11:38:10 +02:00
parent 4baa665693
commit 22638dcc51
7 changed files with 403 additions and 2 deletions

3
.gitignore vendored
View File

@ -1,3 +1,4 @@
atsebay.t
bindata.go
vendor
vendor
.gitlab-oauth-token

7
api.go
View File

@ -15,6 +15,12 @@ func declareAPIRoutes(router *gin.Engine) {
declareAPISurveysRoutes(apiRoutes)
declareAPIWorksRoutes(apiRoutes)
authRoutes := router.Group("")
authRoutes.Use(authMiddleware(loggedUser))
adminRoutes := router.Group("")
adminRoutes.Use(authMiddleware(adminRestricted))
initializeGitLabOIDC(router, authRoutes, adminRoutes)
apiAuthRoutes := router.Group("/api")
apiAuthRoutes.Use(authMiddleware(loggedUser))
@ -43,6 +49,7 @@ func declareAPIRoutes(router *gin.Engine) {
declareAPIAuthGradesRoutes(apiAdminRoutes)
declareAPIAdminHelpRoutes(apiAdminRoutes)
declareAPIAdminQuestionsRoutes(apiAdminRoutes)
declareAPIAuthRepositoriesRoutes(apiAdminRoutes)
declareAPIAdminSurveysRoutes(apiAdminRoutes)
declareAPIAdminUsersRoutes(apiAdminRoutes)
declareAPIAdminWorksRoutes(apiAdminRoutes)

14
db.go
View File

@ -223,6 +223,20 @@ CREATE TABLE IF NOT EXISTS user_work_grades(
FOREIGN KEY(id_user) REFERENCES users(id_user),
FOREIGN KEY(id_work) REFERENCES works(id_work)
) DEFAULT CHARACTER SET = utf8 COLLATE = utf8_bin;
`); err != nil {
return err
}
if _, err := db.Exec(`
CREATE TABLE IF NOT EXISTS user_work_repositories(
id_repository INTEGER NOT NULL PRIMARY KEY AUTO_INCREMENT,
id_user INTEGER NOT NULL,
id_work INTEGER NOT NULL,
uri VARCHAR(255) NOT NULL,
last_check TIMESTAMP,
FOREIGN KEY(id_user) REFERENCES users(id_user),
FOREIGN KEY(id_work) REFERENCES works(id_work),
UNIQUE one_repo_per_work (id_user, id_work)
) DEFAULT CHARACTER SET = utf8 COLLATE = utf8_bin;
`); err != nil {
return err
}

163
gitlab.go Normal file
View File

@ -0,0 +1,163 @@
package main
import (
"context"
"encoding/hex"
"encoding/json"
"flag"
"log"
"net/http"
"os"
"golang.org/x/oauth2"
"github.com/coreos/go-oidc/v3/oidc"
"github.com/gin-gonic/gin"
)
const (
OAUTH_GITLAB_FILE = ".gitlab-oauth-token"
)
var (
gitlabBaseURL = "https://gitlab.cri.epita.fr"
gitlabClientID = ""
gitlabSecret = ""
gitlaboauth2Config oauth2.Config
gitlaboidcVerifier *oidc.IDTokenVerifier
gitlabToken *oauth2.Token
)
func init() {
flag.StringVar(&gitlabClientID, "gitlab-clientid", gitlabClientID, "ClientID for GitLab's OIDC")
flag.StringVar(&gitlabSecret, "gitlab-secret", gitlabSecret, "Secret for GitLab's OIDC")
}
func initializeGitLabOIDC(router *gin.Engine, authrouter *gin.RouterGroup, adminrouter *gin.RouterGroup) {
adminrouter.GET("/auth/gitlabcri", redirectOAuth_GitLab)
router.GET("/callback/gitlabcri/complete", GitLab_OAuth_complete)
if _, err := os.Stat(OAUTH_GITLAB_FILE); err == nil {
gitlabToken, err = loadOAuth2Token(OAUTH_GITLAB_FILE)
if err != nil {
log.Println("Unable to load OAuth2 Token:", err.Error())
}
}
if gitlabClientID != "" && gitlabSecret != "" {
provider, err := oidc.NewProvider(context.Background(), gitlabBaseURL)
if err != nil {
log.Fatal("Unable to setup oidc:", err)
}
gitlaboauth2Config = oauth2.Config{
ClientID: gitlabClientID,
ClientSecret: gitlabSecret,
RedirectURL: oidcRedirectURL + baseURL + "/callback/gitlabcri/complete",
// Discovery returns the OAuth2 endpoints.
Endpoint: provider.Endpoint(),
// "openid" is a required scope for OpenID Connect flows.
Scopes: []string{"api", "read_repository", "email"},
}
oidcConfig := oidc.Config{
ClientID: gitlabClientID,
}
gitlaboidcVerifier = provider.Verifier(&oidcConfig)
authrouter.GET("/api/gitlabcri/repositories", GitLab_getRepositories)
}
}
func loadOAuth2Token(file string) (*oauth2.Token, error) {
fd, err := os.Open(file)
if err != nil {
return nil, err
}
defer fd.Close()
var tok oauth2.Token
err = json.NewDecoder(fd).Decode(&tok)
return &tok, err
}
func saveOAuth2Token(file string, tok *oauth2.Token) error {
fd, err := os.Create(file)
if err != nil {
return err
}
defer fd.Close()
return json.NewEncoder(fd).Encode(tok)
}
func redirectOAuth_GitLab(c *gin.Context) {
session := c.MustGet("Session").(*Session)
// Save next parameter
if len(c.Request.URL.Query().Get("next")) > 0 {
session.SetKey("gitlab-oidc-source", c.Request.URL.Query().Get("next"))
}
c.Redirect(http.StatusFound, gitlaboauth2Config.AuthCodeURL(hex.EncodeToString(session.Id)))
}
func GitLab_OAuth_complete(c *gin.Context) {
idsession, err := hex.DecodeString(c.Request.URL.Query().Get("state"))
if err != nil {
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": err.Error()})
return
}
session, err := getSession(idsession)
if err != nil {
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": err.Error()})
return
}
oauth2Token, err := gitlaboauth2Config.Exchange(c.Request.Context(), c.Request.URL.Query().Get("code"))
if err != nil {
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "Failed to exchange token: " + err.Error()})
return
}
gitlabToken = oauth2Token
err = saveOAuth2Token(OAUTH_GITLAB_FILE, oauth2Token)
if err != nil {
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "Unable to save OAuth2 token: " + err.Error()})
return
}
log.Println("New GitLab OAuth2 session opened")
if source, ok := session.GetKey("gitlab-oidc-source"); ok {
session.DeleteKey("gitlab-oidc-source")
c.Redirect(http.StatusFound, baseURL+source.(string))
} else {
c.Redirect(http.StatusFound, baseURL+"/works")
}
session.Update()
}
func GitLab_getRepositories(c *gin.Context) {
client := gitlaboauth2Config.Client(c.Request.Context(), gitlabToken)
req, err := http.NewRequest("GET", gitlabBaseURL+"/api/v4/projects", nil)
if err != nil {
log.Println("Unable to create NewRequest before GitLab call: ", err.Error())
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs when performing the GitLab request."})
return
}
resp, err := client.Do(req)
if err != nil {
log.Println("Unable to perform the GitLab request: ", err.Error())
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs when performing the GitLab request."})
return
}
c.DataFromReader(resp.StatusCode, resp.ContentLength, resp.Header.Get("content-type"), resp.Body, nil)
}

207
repositories.go Normal file
View File

@ -0,0 +1,207 @@
package main
import (
"fmt"
"log"
"net/http"
"strconv"
"time"
"github.com/gin-gonic/gin"
)
func declareAPIAuthRepositoriesRoutes(router *gin.RouterGroup) {
router.GET("/repositories", func(c *gin.Context) {
var u *User
if user, ok := c.Get("user"); ok {
u = user.(*User)
} else {
u = c.MustGet("LoggedUser").(*User)
}
repositories, err := u.GetRepositories()
if err != nil {
log.Println("Unable to GetRepositories:", err)
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "Unable to retrieve your repositories. Please try again in a few moment."})
return
}
c.JSON(http.StatusOK, repositories)
})
router.POST("/repositories", func(c *gin.Context) {
var u *User
if user, ok := c.Get("user"); ok {
u = user.(*User)
} else {
u = c.MustGet("LoggedUser").(*User)
}
var repository Repository
if err := c.ShouldBindJSON(&repository); err != nil {
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": err.Error()})
return
}
w := c.MustGet("work").(*Work)
k, err := u.NewRepository(w, repository.URI)
if err != nil {
log.Println("Unable to NewRepository:", err)
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "Unable to register your public repository. Please try again in a few moment."})
return
}
c.JSON(http.StatusOK, k)
})
repositoriesRoutes := router.Group("/repositories/:rid")
repositoriesRoutes.Use(repositoryHandler)
repositoriesRoutes.GET("", func(c *gin.Context) {
repo := c.MustGet("repository").(*Repository)
c.JSON(http.StatusOK, repo)
})
repositoriesRoutes.PUT("", func(c *gin.Context) {
current := c.MustGet("repository").(*Repository)
var new Repository
if err := c.ShouldBindJSON(&new); err != nil {
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": err.Error()})
return
}
new.Id = current.Id
u := c.MustGet("LoggedUser").(*User)
if new.IdUser != current.IdUser && !u.IsAdmin {
c.AbortWithStatusJSON(http.StatusForbidden, gin.H{"errmsg": "Operation not allowed."})
return
}
if repository, err := new.Update(); err != nil {
log.Println("Unable to Update repository:", err)
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": fmt.Sprintf("An error occurs during repository updation: %s", err.Error())})
return
} else {
c.JSON(http.StatusOK, repository)
}
})
repositoriesRoutes.DELETE("", func(c *gin.Context) {
repository := c.MustGet("repository").(*Repository)
if _, err := repository.Delete(); err != nil {
log.Println("Unable to Delete repository:", err)
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": fmt.Sprintf("An error occurs during repository deletion: %s", err.Error())})
return
} else {
c.JSON(http.StatusOK, nil)
}
})
}
func repositoryHandler(c *gin.Context) {
var u *User
if user, ok := c.Get("user"); ok {
u = user.(*User)
} else {
u = c.MustGet("LoggedUser").(*User)
}
if rid, err := strconv.Atoi(string(c.Param("rid"))); err != nil {
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": "Bad repository identifier."})
return
} else if u.IsAdmin {
if repository, err := getRepository(rid); err != nil {
c.AbortWithStatusJSON(http.StatusNotFound, gin.H{"errmsg": "Repository not found."})
return
} else {
c.Set("repository", repository)
c.Next()
}
} else if repository, err := u.getRepository(rid); err != nil {
c.AbortWithStatusJSON(http.StatusNotFound, gin.H{"errmsg": "Repository not found."})
return
} else {
c.Set("repository", repository)
c.Next()
}
}
type Repository struct {
Id int64 `json:"id"`
IdUser int64 `json:"id_user"`
IdWork int64 `json:"id_work"`
URI string `json:"uri"`
LastCheck *time.Time `json:"last_check"`
}
func (u *User) GetRepositories() (repositories []*Repository, err error) {
if rows, errr := DBQuery("SELECT id_repository, id_user, id_work, uri, last_check 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); err != nil {
return
}
repositories = append(repositories, &repo)
}
if err = rows.Err(); err != nil {
return
}
return
}
}
func getRepository(id int) (r *Repository, err error) {
r = new(Repository)
err = DBQueryRow("SELECT id_repository, id_user, id_work, uri, lastcheck FROM user_work_repositories WHERE id_repository=?", id).Scan(&r.Id, &r.IdUser, &r.IdWork, &r.URI, &r.LastCheck)
return
}
func (u *User) getRepository(id int) (r *Repository, err error) {
r = new(Repository)
err = DBQueryRow("SELECT id_repository, id_user, id_work, uri FROM user_work_repositories WHERE id_repository=? AND id_user=?", id, u.Id).Scan(&r.Id, &r.IdUser, &r.IdWork, &r.URI, &r.LastCheck)
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) VALUES (?, ?, ?)", u.Id, w.Id, uri); 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
}
}
func (r *Repository) Update() (*Repository, error) {
if _, err := DBExec("UPDATE user_work_repositories SET id_user = ?, id_work = ?, uri = ?, last_check = ? WHERE id_repository = ?", r.IdUser, r.IdWork, r.URI, r.LastCheck, r.Id); err != nil {
return nil, err
} else {
return r, err
}
}
func (r Repository) Delete() (int64, error) {
if res, err := DBExec("DELETE FROM user_work_repositories WHERE id_repository = ?", r.Id); err != nil {
return 0, err
} else if nb, err := res.RowsAffected(); err != nil {
return 0, err
} else {
return nb, err
}
}
func ClearRepositories() (int64, error) {
if res, err := DBExec("DELETE FROM user_work_repositories"); err != nil {
return 0, err
} else if nb, err := res.RowsAffected(); err != nil {
return 0, err
} else {
return nb, err
}
}

View File

@ -13,7 +13,14 @@
</script>
{#if $user && $user.is_admin}
<a href="grades" class="btn btn-success ml-1 float-end" title="Notes">
<a
href="auth/gitlabcri?next={window.location.pathname}"
class="btn btn-primary float-end"
>
<i class="bi bi-link-45deg"></i>
OAuth GitLab
</a>
<a href="grades" class="btn btn-success me-1 float-end" title="Notes">
<i class="bi bi-files"></i>
</a>
{#await getPromos() then promos}

View File

@ -217,6 +217,8 @@ func declareAPIAuthWorksRoutes(router *gin.RouterGroup) {
c.JSON(http.StatusOK, g)
}
})
declareAPIAuthRepositoriesRoutes(worksRoutes)
}
func workHandler(c *gin.Context) {