From a33cd9ac2da8ac97733e52ed553954b0bb857031 Mon Sep 17 00:00:00 2001 From: Andrea Fazzi Date: Mon, 18 Nov 2019 12:40:28 +0100 Subject: [PATCH] Quick prototyping of user authentication/authorization --- handlers/handlers.go | 2 +- handlers/handlers_test.go | 463 --------------------------- handlers/login.go | 12 +- orm/contest.go | 23 +- orm/participant.go | 35 ++ renderer/funcmap.go | 11 + renderer/renderer.go | 13 +- templates/layout/base.html.tpl | 4 +- templates/participants_show.html.tpl | 5 +- 9 files changed, 94 insertions(+), 474 deletions(-) delete mode 100644 handlers/handlers_test.go diff --git a/handlers/handlers.go b/handlers/handlers.go index e086a6c3..047a44c6 100644 --- a/handlers/handlers.go +++ b/handlers/handlers.go @@ -298,7 +298,7 @@ func modelHandler(model string, pattern PathPattern) http.Handler { func homeHandler() http.Handler { fn := func(w http.ResponseWriter, r *http.Request) { - http.Redirect(w, r, "/questions?format=html&tpl_layout=base&tpl_content=questions", http.StatusSeeOther) + http.Redirect(w, r, "/contests?format=html&tpl_layout=base&tpl_content=contests", http.StatusSeeOther) } return http.HandlerFunc(fn) } diff --git a/handlers/handlers_test.go b/handlers/handlers_test.go deleted file mode 100644 index 0b46e145..00000000 --- a/handlers/handlers_test.go +++ /dev/null @@ -1,463 +0,0 @@ -package handlers - -import ( - "bytes" - "encoding/json" - "fmt" - "net/http" - "net/http/httptest" - "net/url" - "strings" - "testing" - "time" - - "github.com/gorilla/mux" - "github.com/jinzhu/gorm" - "github.com/remogatto/prettytest" - "gogs.carducci-dante.gov.it/karmen/core/config" - "gogs.carducci-dante.gov.it/karmen/core/orm" - "gogs.carducci-dante.gov.it/karmen/core/renderer" -) - -var ( - token string -) - -// Start of setup - -type testSuite struct { - prettytest.Suite -} - -func TestRunner(t *testing.T) { - prettytest.Run( - t, - new(testSuite), - ) -} - -func (t *testSuite) BeforeAll() { - - var ( - db *gorm.DB - err error - ) - - // Initialize the ORM - - connected := false - for !connected { - time.Sleep(10 * time.Second) - db, err = orm.New("karmen:karmen@/karmen_test?charset=utf8&parseTime=True&loc=Local") - if err != nil { - time.Sleep(5 * time.Second) - continue - } - connected = true - } - - orm.Use(db) - orm.AutoMigrate() - - // Initialize the renderers - - htmlRenderer, err := renderer.NewHTMLRenderer("./testdata/templates/") - if err != nil { - panic(err) - } - - jsonRenderer, err := renderer.NewJSONRenderer() - if err != nil { - panic(err) - } - - csvRenderer, err := renderer.NewCSVRenderer() - if err != nil { - panic(err) - } - - renderer.Render = make(map[string]func(http.ResponseWriter, *http.Request, interface{}, ...url.Values)) - - renderer.Render["html"] = func(w http.ResponseWriter, r *http.Request, data interface{}, options ...url.Values) { - htmlRenderer.Render(w, r, data, options...) - } - - renderer.Render["json"] = func(w http.ResponseWriter, r *http.Request, data interface{}, options ...url.Values) { - jsonRenderer.Render(w, r, data, options...) - } - - renderer.Render["csv"] = func(w http.ResponseWriter, r *http.Request, data interface{}, options ...url.Values) { - csvRenderer.Render(w, r, data, options...) - } - - // Load the configuration - - err = config.ReadFile("testdata/config.yaml", config.Config) - if err != nil { - panic(err) - } - config.Config.LogLevel = config.LOG_LEVEL_OFF - - req, err := http.NewRequest("GET", "/get_token", nil) - if err != nil { - panic(err) - } - - req.SetBasicAuth("admin", "admin") - - rr := httptest.NewRecorder() - tokenHandler().ServeHTTP(rr, req) - - var data struct { - Token string - UserID string - } - - if err := json.Unmarshal(rr.Body.Bytes(), &data); err != nil { - panic(err) - } - - token = data.Token - -} - -func (t *testSuite) TestGetTeachersHTML() { - req, err := http.NewRequest("GET", "/teachers?format=html&tpl_layout=base&tpl_content=teachers", nil) - if err != nil { - panic(err) - } - - pattern := PathPattern{ - "/%s", - "/%s?format=html&tpl_layout=base&tpl_content=%s", - []string{"GET"}, - } - - rr := httptest.NewRecorder() - modelHandler("teachers", pattern).ServeHTTP(rr, req) - - t.Equal(http.StatusOK, rr.Code) - - if !t.Failed() { - t.True(strings.Contains(rr.Body.String(), "DELLE ROSE")) - } - -} - -func (t *testSuite) TestGetTeachersJSON() { - var ( - teachers []*orm.Teacher - response renderer.JsonResponse - ) - - req, err := http.NewRequest("GET", "/api/teachers?format=json", nil) - if err != nil { - panic(err) - } - - pattern := PathPattern{ - "/api/%s", - "/%s/%d?format=json", - []string{"GET"}, - } - - rr := httptest.NewRecorder() - modelHandler("teachers", pattern).ServeHTTP(rr, req) - - t.Equal(http.StatusOK, rr.Code) - - if !t.Failed() { - err := json.Unmarshal(rr.Body.Bytes(), &response) - t.Nil(err) - if !t.Failed() { - err := json.Unmarshal(response.Result, &teachers) - t.Nil(err) - t.Equal("AGOSTINO", teachers[0].Surname) - } - } - -} - -func (t *testSuite) TestDeleteActivityJSON() { - var response renderer.JsonResponse - - req, err := http.NewRequest("DELETE", fmt.Sprintf("/api/activities/%d/delete?format=json", 1), nil) - if err != nil { - panic(err) - } - req.Header.Set("Content-Type", "application/json; charset=utf-8") - - rr := httptest.NewRecorder() - - pattern := PathPattern{"/api/%s/{id}/delete", "", []string{"DELETE"}} - router := mux.NewRouter() - router.Handle("/api/activities/{id}/delete", modelHandler("activities", pattern)) - router.ServeHTTP(rr, req) - - if !t.Failed() { - err := json.Unmarshal(rr.Body.Bytes(), &response) - t.Nil(err) - if !t.Failed() { - t.Equal("1", string(response.Result)) - } - } - - t.Equal(http.StatusOK, rr.Code) -} - -func (t *testSuite) TestGetTeacherJSON() { - var ( - teacher *orm.Teacher - response renderer.JsonResponse - ) - - req, err := http.NewRequest("GET", "/api/teachers/9?format=json", nil) - if err != nil { - panic(err) - } - - pattern := PathPattern{"/api/%s/{id}", "", []string{"GET"}} - - rr := httptest.NewRecorder() - - router := mux.NewRouter() - router.Handle("/api/teachers/{id}", modelHandler("teachers", pattern)) - router.ServeHTTP(rr, req) - - t.Equal(http.StatusOK, rr.Code) - - if !t.Failed() { - err := json.Unmarshal(rr.Body.Bytes(), &response) - t.Nil(err) - if !t.Failed() { - err := json.Unmarshal(response.Result, &teacher) - t.Nil(err) - t.Equal("FRANCESCHINI", teacher.Surname) - } - } - -} - -func (t *testSuite) TestGetTeachersCSV() { - var response renderer.JsonResponse - - req, err := http.NewRequest("GET", "/api/teachers?format=csv", nil) - if err != nil { - panic(err) - } - - pattern := PathPattern{ - "/api/%s", - "/%s/%d?format=csv", - []string{"GET"}, - } - - rr := httptest.NewRecorder() - modelHandler("teachers", pattern).ServeHTTP(rr, req) - - t.Equal(http.StatusOK, rr.Code) - - if !t.Failed() { - err := json.Unmarshal(rr.Body.Bytes(), &response) - t.Nil(err) - if !t.Failed() { - t.True(strings.Contains(string(response.Result), "AGOSTINO")) - } - } - -} - -func (t *testSuite) TestGetErrorJSON() { - var ( - response renderer.JsonResponse - ) - - req, err := http.NewRequest("GET", "/api/teacher/100?format=json", nil) - if err != nil { - panic(err) - } - - pattern := PathPattern{"/api/%s/{id}", "/%s?format=json", []string{"GET"}} - - rr := httptest.NewRecorder() - modelHandler("teachers", pattern).ServeHTTP(rr, req) - - err = json.Unmarshal(rr.Body.Bytes(), &response) - t.Nil(err) - t.Equal("record not found", string(response.Error)) - -} - -func (t *testSuite) TestPostErrorJSON() { - var ( - response renderer.JsonResponse - ) - - teacher := getTeacherJSON(1) - teacher.Name = "Mario" - teacher.Surname = "ROSSI" - - data, err := json.Marshal(teacher) - if err != nil { - panic(err) - } - - req, err := http.NewRequest("POST", "/api/teachers/0/update?format=json", bytes.NewBuffer(data)) - if err != nil { - panic(err) - } - req.Header.Set("Content-Type", "application/json; charset=utf-8") - - rr := httptest.NewRecorder() - - pattern := PathPattern{"/api/%s/{id}/update", "", []string{"POST"}} - router := mux.NewRouter() - router.Handle("/api/teachers/{id}/update", modelHandler("teachers", pattern)) - router.ServeHTTP(rr, req) - - t.Equal(http.StatusInternalServerError, rr.Code) - - err = json.Unmarshal(rr.Body.Bytes(), &response) - t.Nil(err) - t.Equal("record not found", string(response.Error)) - -} - -func (t *testSuite) TestAddTeacherJSON() { - var ( - response renderer.JsonResponse - id uint - ) - - teacher := new(orm.Teacher) - teacher.Name = "Mario" - teacher.Surname = "ROSSI" - - data, err := json.Marshal(teacher) - t.Nil(err) - - req, err := http.NewRequest("POST", "/api/teachers/add", bytes.NewBuffer(data)) - req.Header.Set("Content-Type", "application/json; charset=utf-8") - - if err != nil { - panic(err) - } - - pattern := PathPattern{ - "/api/%s/add", - "", - []string{"POST"}, - } - - rr := httptest.NewRecorder() - modelHandler("teachers", pattern).ServeHTTP(rr, req) - - t.Equal(http.StatusOK, rr.Code) - - if !t.Failed() { - err := json.Unmarshal(rr.Body.Bytes(), &response) - t.Nil(err) - if !t.Failed() { - err := json.Unmarshal(response.Result, &id) - t.Nil(err) - t.Equal(uint(10), id) - } - } - -} - -func (t *testSuite) TestUpdateTeacherJSON() { - teacher := getTeacherJSON(1) - teacher.Name = "Mario" - teacher.Surname = "ROSSI" - - data, err := json.Marshal(teacher) - if err != nil { - panic(err) - } - - req, err := http.NewRequest("POST", fmt.Sprintf("/api/teachers/%d/update?format=json", teacher.ID), bytes.NewBuffer(data)) - if err != nil { - panic(err) - } - req.Header.Set("Content-Type", "application/json; charset=utf-8") - - rr := httptest.NewRecorder() - - pattern := PathPattern{"/api/%s/{id}/update", "", []string{"POST"}} - router := mux.NewRouter() - router.Handle("/api/teachers/{id}/update", modelHandler("teachers", pattern)) - router.ServeHTTP(rr, req) - - t.Equal(http.StatusOK, rr.Code) - - if !t.Failed() { - dbTeacher := getTeacherJSON(1) - t.Equal("ROSSI", dbTeacher.Surname) - } - -} - -func getTeacherJSON(id uint) *orm.Teacher { - var ( - teacher *orm.Teacher - response renderer.JsonResponse - ) - - req, err := http.NewRequest("GET", fmt.Sprintf("/api/teachers/%d?format=json", id), nil) - if err != nil { - panic(err) - } - - pattern := PathPattern{"/api/%s/{id}", "", []string{"GET"}} - - rr := httptest.NewRecorder() - - router := mux.NewRouter() - router.Handle("/api/teachers/{id}", modelHandler("teachers", pattern)) - router.ServeHTTP(rr, req) - - err = json.Unmarshal(rr.Body.Bytes(), &response) - if err != nil { - panic(err) - } - err = json.Unmarshal(response.Result, &teacher) - if err != nil { - panic(err) - } - return teacher - -} - -func (t *testSuite) TestGetDepartmentJSON() { - var ( - department *orm.Department - response renderer.JsonResponse - ) - - req, err := http.NewRequest("GET", "/api/departments/1?format=json", nil) - if err != nil { - panic(err) - } - - pattern := PathPattern{"/api/%s/{id}", "", []string{"GET"}} - - rr := httptest.NewRecorder() - - router := mux.NewRouter() - router.Handle("/api/departments/{id}", modelHandler("departments", pattern)) - router.ServeHTTP(rr, req) - - t.Equal(http.StatusOK, rr.Code) - - if !t.Failed() { - err := json.Unmarshal(rr.Body.Bytes(), &response) - t.Nil(err) - if !t.Failed() { - err := json.Unmarshal(response.Result, &department) - t.Nil(err) - t.Equal("LINGUE STRANIERE", department.Name) - } - } - -} diff --git a/handlers/login.go b/handlers/login.go index 45814243..db6494bd 100644 --- a/handlers/login.go +++ b/handlers/login.go @@ -7,6 +7,7 @@ import ( "time" "git.andreafazzi.eu/andrea/oef/config" + "git.andreafazzi.eu/andrea/oef/orm" "git.andreafazzi.eu/andrea/oef/renderer" jwt "github.com/dgrijalva/jwt-go" ) @@ -54,11 +55,20 @@ func loginHandler() http.Handler { return http.HandlerFunc(fn) } +// FIXME: This is an hack for fast prototyping: users should have +// their own table on DB. func checkCredential(username string, password string) (*User, error) { + var participant orm.Participant + if username == config.Config.Admin.Username && password == config.Config.Admin.Password { return &User{username, true}, nil } - return nil, errors.New("Authentication failed!") + + if err := orm.DB().Where("username = ? AND password = ?", username, password).First(&participant).Error; err != nil { + return nil, errors.New("Authentication failed!") + } else { + return &User{username, false}, nil + } } func getToken(username string, password string) ([]byte, error) { diff --git a/orm/contest.go b/orm/contest.go index cf9f6830..a122fd3a 100644 --- a/orm/contest.go +++ b/orm/contest.go @@ -7,6 +7,7 @@ import ( "time" "git.andreafazzi.eu/andrea/oef/renderer" + "github.com/dgrijalva/jwt-go" "github.com/jinzhu/gorm" ) @@ -82,10 +83,28 @@ func (c *Contest) Read(args map[string]string, r *http.Request) (interface{}, er func (c *Contest) ReadAll(args map[string]string, r *http.Request) (interface{}, error) { var contests []*Contest - if err := DB().Order("created_at").Find(&contests).Error; err != nil { + + claims := r.Context().Value("user").(*jwt.Token).Claims.(jwt.MapClaims) + + if claims["admin"].(bool) { + if err := DB().Order("created_at").Find(&contests).Error; err != nil { + return nil, err + } else { + return contests, nil + } + } + + participant := &Participant{} + + if err := DB().Where("username = ?", claims["name"].(string)).First(&participant).Error; err != nil { return nil, err } - return contests, nil + + if err := DB().Debug().Order("created_at").Find(&participant.Contests).Error; err != nil { + return nil, err + } else { + return participant.Contests, nil + } } func (c *Contest) Update(args map[string]string, r *http.Request) (interface{}, error) { diff --git a/orm/participant.go b/orm/participant.go index 0b9d4c01..8f7151d6 100644 --- a/orm/participant.go +++ b/orm/participant.go @@ -2,11 +2,13 @@ package orm import ( "fmt" + "net/http" "strings" "git.andreafazzi.eu/andrea/oef/renderer" "github.com/jinzhu/gorm" + "github.com/sethvargo/go-password/password" ) type Participant struct { @@ -25,12 +27,45 @@ type Participant struct { AllContests []*Contest `gorm:"-"` } +func (model *Participant) sanitize(s string) string { + lower := strings.ToLower(s) + r := strings.NewReplacer("'", "", "-", "", " ", "", "ò", "o", "ì", "i") + return r.Replace(lower) +} + +func (model *Participant) genUsername() error { + model.Username = strings.Join([]string{model.sanitize(model.Firstname), model.sanitize(model.Lastname)}, ".") + return nil +} + +func (model *Participant) genPassword() error { + password, err := password.Generate(8, 2, 0, false, true) + if err != nil { + return err + } + model.Password = password + return nil + +} + func (model *Participant) GetID() uint { return model.ID } func (model *Participant) String() string { return fmt.Sprintf("%s %s", strings.ToUpper(model.Lastname), strings.Title(strings.ToLower(model.Firstname))) } +func (model *Participant) BeforeSave(tx *gorm.DB) error { + if err := model.genUsername(); err != nil { + return err + } + if model.Password == "" { + if err := model.genPassword(); err != nil { + return err + } + } + return nil +} + func (model *Participant) Create(args map[string]string, r *http.Request) (interface{}, error) { if r.Method == "GET" { participant := new(Participant) diff --git a/renderer/funcmap.go b/renderer/funcmap.go index 4aef994b..3192a281 100644 --- a/renderer/funcmap.go +++ b/renderer/funcmap.go @@ -9,6 +9,7 @@ import ( "strings" "time" + jwt "github.com/dgrijalva/jwt-go" "github.com/jinzhu/inflection" yml "gopkg.in/yaml.v2" ) @@ -46,9 +47,19 @@ var ( "pluralize": pluralize, "lower": lower, "trim": trim, + "username": username, + "isAdmin": isAdmin, } ) +func username(claims jwt.MapClaims) string { + return claims["name"].(string) +} + +func isAdmin(claims jwt.MapClaims) bool { + return claims["admin"].(bool) +} + func trim(text string) string { if len(text) > MaxTextLength { return text[0:MaxTextLength] + "…" diff --git a/renderer/renderer.go b/renderer/renderer.go index 980030d6..67f239cf 100644 --- a/renderer/renderer.go +++ b/renderer/renderer.go @@ -35,6 +35,7 @@ type PDFRenderer struct{} type htmlTemplateData struct { Data interface{} Options url.Values + Claims jwt.MapClaims } type JsonResponse struct { @@ -239,22 +240,24 @@ func (rend *HTMLRenderer) writeError(w http.ResponseWriter, r *http.Request, dat func (rend *HTMLRenderer) Render(w http.ResponseWriter, r *http.Request, data interface{}, options ...url.Values) { if data != nil && isErrorType(data) { - rend.writeError(w, r, &htmlTemplateData{data.(error), nil}) + rend.writeError(w, r, &htmlTemplateData{data.(error), nil, nil}) } else { t, ok := rend.templates[options[0]["tpl_content"][0]] if !ok { err := fmt.Errorf("Template %s not found", options[0]["tpl_content"][0]) - rend.writeError(w, r, &htmlTemplateData{err, nil}) + rend.writeError(w, r, &htmlTemplateData{err, nil, nil}) } + var claims jwt.MapClaims + if r.Context().Value("user") != nil { - log.Println("Current user is", r.Context().Value("user").(*jwt.Token).Claims.(jwt.MapClaims)["name"]) + claims = r.Context().Value("user").(*jwt.Token).Claims.(jwt.MapClaims) } w.Header().Set("Content-Type", "text/html; charset=utf-8") - err := t.ExecuteTemplate(w, options[0]["tpl_layout"][0], &htmlTemplateData{data, options[0]}) + err := t.ExecuteTemplate(w, options[0]["tpl_layout"][0], &htmlTemplateData{data, options[0], claims}) if err != nil { - rend.writeError(w, r, &htmlTemplateData{err, nil}) + rend.writeError(w, r, &htmlTemplateData{err, nil, nil}) } } } diff --git a/templates/layout/base.html.tpl b/templates/layout/base.html.tpl index f5f330db..dc840d44 100644 --- a/templates/layout/base.html.tpl +++ b/templates/layout/base.html.tpl @@ -23,13 +23,15 @@ diff --git a/templates/participants_show.html.tpl b/templates/participants_show.html.tpl index 8fb217b0..1465109d 100644 --- a/templates/participants_show.html.tpl +++ b/templates/participants_show.html.tpl @@ -7,7 +7,10 @@

Informazioni generali

- Questa scheda contiene la informazioni relative al partecipante {{.Data|string}}. + Il nome utente del partecipante è {{.Data.Username}} +

+

+ La sua password è {{.Data.Password}}