Quick prototyping of user authentication/authorization

This commit is contained in:
Andrea Fazzi 2019-11-18 12:40:28 +01:00
parent 945a2a29dd
commit a33cd9ac2d
9 changed files with 94 additions and 474 deletions

View file

@ -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)
}

View file

@ -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)
}
}
}

View file

@ -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
}
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) {

View file

@ -7,6 +7,7 @@ import (
"time"
"git.andreafazzi.eu/andrea/oef/renderer"
"github.com/dgrijalva/jwt-go"
"github.com/jinzhu/gorm"
)
@ -82,11 +83,29 @@ 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
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
}
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) {
if r.Method == "GET" {

View file

@ -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)

View file

@ -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] + "…"

View file

@ -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})
}
}
}

View file

@ -23,13 +23,15 @@
<ul class="navbar-nav mr-auto">
<a class="nav-item nav-link {{.Options|active "Contest"}}" href="{{all "Contest"}}">Gare</a>
{{if (.Claims|isAdmin)}}
<a class="nav-item nav-link {{.Options|active "Question"}}" href="{{all "Question"}}">Domande</a>
<a class="nav-item nav-link {{.Options|active "Answer"}}" href="{{all "Answer"}}">Risposte</a>
<a class="nav-item nav-link {{.Options|active "Participant"}}" href="{{all "Participant"}}">Partecipanti</a>
{{end}}
</ul>
<ul class="nav navbar-nav navbar-right">
<li><a class="nav-link" href="/logout">Esci</a></li>
<li><a class="nav-link" href="/logout">Disconnetti {{.Claims|username}}</a></li>
</ul>
</div><!--/.nav-collapse -->
</nav>

View file

@ -7,7 +7,10 @@
<h2 class="karmen-relation-header">Informazioni generali</h2>
<p>
Questa scheda contiene la informazioni relative al partecipante <strong>{{.Data|string}}</strong>.
Il nome utente del partecipante è <strong>{{.Data.Username}}</strong>
</p>
<p>
La sua password è <strong>{{.Data.Password}}</strong>
</p>
<div class="row">
<div class="col-md-12">