Check admin code for admin pages

This commit is contained in:
Brandon Dyck 2020-10-04 16:03:16 -06:00
parent 3ad9b2955f
commit 4eca005046
4 changed files with 135 additions and 3 deletions

View File

@ -3,6 +3,7 @@ package back
import ( import (
"context" "context"
"errors" "errors"
"fmt"
"log" "log"
"os" "os"
@ -13,6 +14,15 @@ import (
var ErrNotFound = errors.New("not found") var ErrNotFound = errors.New("not found")
type UnauthorizedError struct {
EventID string
AdminCode string
}
func (u UnauthorizedError) Error() string {
return fmt.Sprintf("unauthorized: EventID = %s, AdminCode = %s", u.EventID, u.AdminCode)
}
type GenString func(length int) (string, error) type GenString func(length int) (string, error)
type Store struct { type Store struct {
@ -164,6 +174,37 @@ func (s *Store) CreateEvent(ctx context.Context, cmd CreateEventCommand) (result
return return
} }
type CheckEventAdminCodeQuery struct {
EventID string
AdminCode string
}
func (s *Store) AuthorizeEventAdmin(ctx context.Context, query CheckEventAdminCodeQuery) error {
conn := s.pool.Get(ctx)
defer s.pool.Put(conn)
const dbQuery = `
SELECT 1
FROM event
WHERE id = ? AND admin_code = ?;`
var doesMatch bool
err := sqlitex.Exec(conn, dbQuery,
func(stmt *sqlite.Stmt) error {
doesMatch = true
return nil
}, query.EventID, query.AdminCode)
if err != nil {
return err
}
if !doesMatch {
return UnauthorizedError{
EventID: query.EventID,
AdminCode: query.AdminCode,
}
}
return nil
}
type GetEventMetadataQuery struct { type GetEventMetadataQuery struct {
EventID string EventID string
} }

View File

@ -175,6 +175,7 @@ func TestGetEventResponseSummary(t *testing.T) {
func TestCreateEventResponse(t *testing.T) { func TestCreateEventResponse(t *testing.T) {
store, err := back.NewMemoryStore(back.SecureGenString) store, err := back.NewMemoryStore(back.SecureGenString)
is.New(t).NoErr(err) is.New(t).NoErr(err)
defer store.Close()
createEvent := func(is *is.I) (eventID string) { createEvent := func(is *is.I) (eventID string) {
event, err := store.CreateEvent(context.Background(), back.CreateEventCommand{ event, err := store.CreateEvent(context.Background(), back.CreateEventCommand{
@ -238,3 +239,64 @@ func TestCreateEventResponse(t *testing.T) {
is.Equal(dateHours, response.DateHours) is.Equal(dateHours, response.DateHours)
}) })
} }
func TestAuthorizeEventAdmin(t *testing.T) {
store, err := back.NewMemoryStore(back.SecureGenString)
is.New(t).NoErr(err)
defer store.Close()
t.Run("returns ErrUnauthorized if admin code is wrong", func(t *testing.T) {
is := is.New(t)
event, err := store.CreateEvent(context.Background(), back.CreateEventCommand{
Name: "my event",
Description: "stuff happening",
Earliest: date.Today(),
Latest: date.Today(),
})
is.NoErr(err)
badAdminCode := event.AdminCode + "x"
err = store.AuthorizeEventAdmin(context.Background(), back.CheckEventAdminCodeQuery{
EventID: event.EventID,
AdminCode: badAdminCode,
})
is.Equal(err, back.UnauthorizedError{
EventID: event.EventID,
AdminCode: badAdminCode,
})
})
t.Run("return ErrUnauthorized if event does not exist", func(t *testing.T) {
is := is.New(t)
randString, err := back.SecureGenString(10)
is.NoErr(err)
err = store.AuthorizeEventAdmin(context.Background(), back.CheckEventAdminCodeQuery{
EventID: randString,
AdminCode: randString,
})
is.Equal(err, back.UnauthorizedError{
EventID: randString,
AdminCode: randString,
})
})
t.Run("returns nil if admin code is correct", func(t *testing.T) {
is := is.New(t)
event, err := store.CreateEvent(context.Background(), back.CreateEventCommand{
Name: "my event",
Description: "stuff happening",
Earliest: date.Today(),
Latest: date.Today(),
})
is.NoErr(err)
err = store.AuthorizeEventAdmin(context.Background(), back.CheckEventAdminCodeQuery{
EventID: event.EventID,
AdminCode: event.AdminCode,
})
is.NoErr(err)
})
}

View File

@ -2,6 +2,7 @@ package front
import ( import (
"context" "context"
"errors"
"fmt" "fmt"
"io" "io"
"log" "log"
@ -300,7 +301,28 @@ func (h *handler) handleDoCreate(w http.ResponseWriter, r *http.Request) {
http.Redirect(w, r, pathCreateSuccess+"?"+successQuery.Encode(), http.StatusSeeOther) http.Redirect(w, r, pathCreateSuccess+"?"+successQuery.Encode(), http.StatusSeeOther)
} }
func (h *handler) blockUnauthorizedAdmin(w http.ResponseWriter, r *http.Request) bool {
eventID := r.URL.Query().Get(fieldNameEventID)
adminCode := r.URL.Query().Get(fieldNameAdminCode)
err := h.store.AuthorizeEventAdmin(context.Background(), back.CheckEventAdminCodeQuery{
EventID: eventID,
AdminCode: adminCode,
})
var authError back.UnauthorizedError
if errors.As(err, &authError) {
http.Error(w, "Event not found", http.StatusNotFound)
logError(authError)
return true
}
return internalServerError(w, err)
}
func (h *handler) handleCreateSuccess(w http.ResponseWriter, r *http.Request) { func (h *handler) handleCreateSuccess(w http.ResponseWriter, r *http.Request) {
if h.blockUnauthorizedAdmin(w, r) {
return
}
eventID := r.URL.Query().Get(fieldNameEventID) eventID := r.URL.Query().Get(fieldNameEventID)
event, err := h.store.GetEventMetadata(r.Context(), back.GetEventMetadataQuery{ event, err := h.store.GetEventMetadata(r.Context(), back.GetEventMetadataQuery{
EventID: eventID, EventID: eventID,
@ -348,8 +370,13 @@ func (h *handler) handleCreateSuccess(w http.ResponseWriter, r *http.Request) {
} }
func (h *handler) handleAdmin(w http.ResponseWriter, r *http.Request) { func (h *handler) handleAdmin(w http.ResponseWriter, r *http.Request) {
if h.blockUnauthorizedAdmin(w, r) {
return
}
query := r.URL.Query() query := r.URL.Query()
eventID := query.Get(fieldNameEventID) eventID := query.Get(fieldNameEventID)
metadata, err := h.store.GetEventMetadata(context.Background(), back.GetEventMetadataQuery{ metadata, err := h.store.GetEventMetadata(context.Background(), back.GetEventMetadataQuery{
EventID: eventID, EventID: eventID,
}) })

View File

@ -1,11 +1,12 @@
Essential: Essential:
------------ ------------
Show response after submission Show response after submission
Make earliest and latest dates required for creation Require earliest and latest dates for creation
Ensure latest date is at least earliest date
Ensure date span is within a maximum
Add timeout to request context
Prevent blank event names and guest names Prevent blank event names and guest names
Consider some front-end, regex-based field validation Consider some front-end, regex-based field validation
Authenticate admin page with admin code
It might be sufficient to require that when getting the response summary.
Consider redirecting from /create/do to admin Consider redirecting from /create/do to admin
Cleanup: Cleanup:
@ -24,4 +25,5 @@ Make the sqlite pool size configurable
More features: More features:
--------------- ---------------
Show validation errors in a header, rather than on their own page
Allow updating metadata on admin page Allow updating metadata on admin page