Skip to content

Commit

Permalink
Merge pull request #1082 from France-ioi/1057
Browse files Browse the repository at this point in the history
Allow not providing the access token when the task token is provided
  • Loading branch information
GeoffreyHuck authored May 28, 2024
2 parents 79d18de + a7a0d07 commit a9f7dc3
Show file tree
Hide file tree
Showing 11 changed files with 334 additions and 190 deletions.
13 changes: 7 additions & 6 deletions app/api/answers/answers.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,14 +25,15 @@ type answerData struct {
// SetRoutes defines the routes for this package in a route answers.
func (srv *Service) SetRoutes(router chi.Router) {
router.Use(render.SetContentType(render.ContentTypeJSON))
router.Use(auth.UserMiddleware(srv.Base))
router.Get("/items/{item_id}/answers", service.AppHandler(srv.listAnswers).ServeHTTP)
router.Get("/items/{item_id}/best-answer", service.AppHandler(srv.getBestAnswer).ServeHTTP)
router.Get("/answers/{answer_id}", service.AppHandler(srv.getAnswer).ServeHTTP)
router.Post("/answers", service.AppHandler(srv.submit).ServeHTTP)
router.Post("/answers/{answer_id}/generate-task-token", service.AppHandler(srv.generateTaskToken).ServeHTTP)

routerWithParticipant := router.With(service.ParticipantMiddleware(srv.Base))
routerWithAuth := router.With(auth.UserMiddleware(srv.Base))
routerWithAuth.Get("/items/{item_id}/answers", service.AppHandler(srv.listAnswers).ServeHTTP)
routerWithAuth.Get("/items/{item_id}/best-answer", service.AppHandler(srv.getBestAnswer).ServeHTTP)
routerWithAuth.Get("/answers/{answer_id}", service.AppHandler(srv.getAnswer).ServeHTTP)
routerWithAuth.Post("/answers/{answer_id}/generate-task-token", service.AppHandler(srv.generateTaskToken).ServeHTTP)

routerWithParticipant := routerWithAuth.With(service.ParticipantMiddleware(srv.Base))
routerWithParticipant.Get("/items/{item_id}/current-answer", service.AppHandler(srv.getCurrentAnswer).ServeHTTP)
routerWithParticipant.Post("/items/{item_id}/attempts/{attempt_id}/answers", service.AppHandler(srv.answerCreate).ServeHTTP)
routerWithParticipant.Put("/items/{item_id}/attempts/{attempt_id}/answers/current", service.AppHandler(srv.updateCurrentAnswer).ServeHTTP)
Expand Down
67 changes: 63 additions & 4 deletions app/api/answers/submit.feature
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,13 @@ Feature: Submit a new answer
Given the database has the following users:
| login | group_id |
| john | 101 |
And the database has the following table 'groups':
| id | name | type |
| 201 | team | Team |
And the database has the following table 'groups_groups':
| parent_group_id | child_group_id |
| 22 | 13 |
| 201 | 101 |
And the groups ancestors are computed
And the database has the following table 'items':
| id | default_language_tag |
Expand All @@ -20,17 +24,19 @@ Feature: Submit a new answer
And the database has the following table 'permissions_generated':
| group_id | item_id | can_view_generated |
| 101 | 50 | content |
| 201 | 50 | content |
And the database has the following table 'attempts':
| id | participant_id |
| 1 | 101 |
| 1 | 201 |
And the database has the following table 'results':
| attempt_id | participant_id | item_id | hints_requested | hints_cached | submissions | latest_activity_at | started_at |
| 1 | 101 | 50 | [{"rotorIndex":0,"cellRank":0}] | 12 | 2 | 2019-05-30 11:00:00 | 2019-05-30 11:00:00 |
| 1 | 101 | 10 | null | 0 | 0 | 2019-05-30 11:00:00 | 2019-05-30 11:00:00 |
| 1 | 201 | 50 | [{"rotorIndex":0,"cellRank":0}] | 2 | 1 | 2020-01-01 00:00:00 | 2020-01-01 00:00:00 |

Scenario: User is able to submit a new answer
Given I am the user with id "101"
And time is frozen
Given time is frozen
And "userTaskToken" is a token signed by the app with the following payload:
"""
{
Expand Down Expand Up @@ -78,11 +84,63 @@ Feature: Submit a new answer
| attempt_id | participant_id | item_id | hints_requested | hints_cached | submissions | ABS(TIMESTAMPDIFF(SECOND, latest_activity_at, NOW())) < 3 | ABS(TIMESTAMPDIFF(SECOND, latest_submission_at, NOW())) < 3 |
| 1 | 101 | 10 | null | 0 | 0 | 1 | null |
| 1 | 101 | 50 | [{"rotorIndex":0,"cellRank":0}] | 12 | 3 | 1 | 1 |
| 1 | 201 | 50 | [{"rotorIndex":0,"cellRank":0}] | 2 | 1 | 0 | null |
And the table "results_propagate" should be empty

Scenario: User is able to submit a new answer for his team (participant_id is the first integer in the idAttempt in the task token)
Given time is frozen
And "userTaskToken" is a token signed by the app with the following payload:
"""
{
"idUser": "101",
"idItemLocal": "50",
"idAttempt": "201/1",
"platformName": "{{app().Config.GetString("token.platformName")}}"
}
"""
When I send a POST request to "/answers" with the following body:
"""
{
"task_token": "{{userTaskToken}}",
"answer": "print 1"
}
"""
Then the response code should be 201
And the response body decoded as "AnswersSubmitResponse" should be, in JSON:
"""
{
"data": {
"answer_token": {
"date": "{{currentTimeInFormat("02-01-2006")}}",
"idUser": "101",
"idItem": null,
"idAttempt": "201/1",
"itemUrl": "",
"idItemLocal": "50",
"platformName": "algrorea_backend",
"randomSeed": "",
"sHintsRequested": "[{\"rotorIndex\":0,\"cellRank\":0}]",
"nbHintsGiven": "2",
"sAnswer": "print 1",
"idUserAnswer": "5577006791947779410"
}
},
"message": "created",
"success": true
}
"""
And the table "answers" should be:
| author_id | participant_id | attempt_id | item_id | type | answer | ABS(TIMESTAMPDIFF(SECOND, created_at, NOW())) < 3 |
| 101 | 201 | 1 | 50 | Submission | print 1 | 1 |
And the table "results" should be:
| attempt_id | participant_id | item_id | hints_requested | hints_cached | submissions | ABS(TIMESTAMPDIFF(SECOND, latest_activity_at, NOW())) < 3 | ABS(TIMESTAMPDIFF(SECOND, latest_submission_at, NOW())) < 3 |
| 1 | 101 | 10 | null | 0 | 0 | 0 | null |
| 1 | 101 | 50 | [{"rotorIndex":0,"cellRank":0}] | 12 | 2 | 0 | null |
| 1 | 201 | 50 | [{"rotorIndex":0,"cellRank":0}] | 2 | 2 | 1 | 1 |
And the table "results_propagate" should be empty

Scenario: User is able to submit a new answer (with all fields filled in the token)
Given I am the user with id "101"
And time is frozen
Given time is frozen
And "userTaskToken" is a token signed by the app with the following payload:
"""
{
Expand Down Expand Up @@ -134,4 +192,5 @@ Feature: Submit a new answer
| attempt_id | participant_id | item_id | hints_requested | hints_cached | submissions | ABS(TIMESTAMPDIFF(SECOND, latest_activity_at, NOW())) < 3 | ABS(TIMESTAMPDIFF(SECOND, latest_submission_at, NOW())) < 3 |
| 1 | 101 | 10 | null | 0 | 0 | 1 | null |
| 1 | 101 | 50 | [{"rotorIndex":0,"cellRank":0}] | 12 | 3 | 1 | 1 |
| 1 | 201 | 50 | [{"rotorIndex":0,"cellRank":0}] | 2 | 1 | 0 | null |
And the table "results_propagate" should be empty
79 changes: 36 additions & 43 deletions app/api/answers/submit.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,45 @@ import (
"net/http"
"strconv"

"github.com/France-ioi/AlgoreaBackend/app/doc"

"github.com/go-chi/render"
"github.com/jinzhu/gorm"

"github.com/France-ioi/AlgoreaBackend/app/database"
"github.com/France-ioi/AlgoreaBackend/app/doc"
"github.com/France-ioi/AlgoreaBackend/app/service"
"github.com/France-ioi/AlgoreaBackend/app/token"
)

// SubmitRequest represents a JSON request body format needed by answers.submit()
// swagger:ignore
type SubmitRequest struct {
TaskToken *token.Task `json:"task_token"`
Answer *string `json:"answer"`

PublicKey *rsa.PublicKey
}

// swagger:model
type submitRequestWrapper struct {
// required:true
TaskToken *string `json:"task_token"`
// required:true
Answer *string `json:"answer"`
}

// Created. Success response with answer_token
// swagger:model answerSubmitResponse
type answerSubmitResponse struct {
// description
// swagger:allOf
doc.CreatedResponse
// required:true
Data struct {
AnswerToken string `json:"answer_token"`
} `json:"data"`
}

// swagger:operation POST /answers answers itemGetAnswerToken
//
// ---
Expand All @@ -25,9 +55,11 @@ import (
// Generate and return an answer token from user's answer and task token.
// It is used to bind an answer with task parameters so that the TaskGrader can check if they have not been altered.
//
// * task_token.idUser should be the current user.
//
// * The user should have submission rights on `task_token.idItemLocal`.
// This service doesn't require authentication. The user is identified by the task token.
//
//
// * The task token's user should have submission rights on `task_token.idItemLocal`.
//
// * The attempt should allow submission (`attempts.allows_submissions_until` should be a time in the future).
//
Expand All @@ -46,8 +78,6 @@ import (
// "$ref": "#/definitions/answerSubmitResponse"
// "400":
// "$ref": "#/responses/badRequestResponse"
// "401":
// "$ref": "#/responses/unauthorizedResponse"
// "403":
// "$ref": "#/responses/forbiddenResponse"
// "500":
Expand All @@ -60,14 +90,6 @@ func (srv *Service) submit(rw http.ResponseWriter, httpReq *http.Request) servic
return service.ErrInvalidRequest(err)
}

user := srv.GetUser(httpReq)

if user.GroupID != requestData.TaskToken.Converted.UserID {
return service.ErrInvalidRequest(fmt.Errorf(
"token doesn't correspond to user session: got idUser=%d, expected %d",
requestData.TaskToken.Converted.UserID, user.GroupID))
}

var answerID int64
var hintsInfo *database.HintsInfo
apiError := service.NoError
Expand All @@ -94,7 +116,7 @@ func (srv *Service) submit(rw http.ResponseWriter, httpReq *http.Request) servic
service.MustNotBeError(err)

answerID, err = store.Answers().SubmitNewAnswer(
user.GroupID, requestData.TaskToken.Converted.ParticipantID, requestData.TaskToken.Converted.AttemptID,
requestData.TaskToken.Converted.UserID, requestData.TaskToken.Converted.ParticipantID, requestData.TaskToken.Converted.AttemptID,
requestData.TaskToken.Converted.LocalItemID, *requestData.Answer)
service.MustNotBeError(err)

Expand Down Expand Up @@ -139,23 +161,6 @@ func (srv *Service) submit(rw http.ResponseWriter, httpReq *http.Request) servic
return service.NoError
}

// SubmitRequest represents a JSON request body format needed by answers.submit()
// swagger:ignore
type SubmitRequest struct {
TaskToken *token.Task `json:"task_token"`
Answer *string `json:"answer"`

PublicKey *rsa.PublicKey
}

// swagger:model
type submitRequestWrapper struct {
// required:true
TaskToken *string `json:"task_token"`
// required:true
Answer *string `json:"answer"`
}

// UnmarshalJSON loads SubmitRequest from JSON passing a public key into TaskToken.
func (requestData *SubmitRequest) UnmarshalJSON(raw []byte) error {
var wrapper submitRequestWrapper
Expand Down Expand Up @@ -187,15 +192,3 @@ func (requestData *SubmitRequest) Bind(r *http.Request) error {
}

var _ render.Binder = (*SubmitRequest)(nil)

// Created. Success response with answer_token
// swagger:model answerSubmitResponse
type answerSubmitResponse struct {
// description
// swagger:allOf
doc.CreatedResponse
// required:true
Data struct {
AnswerToken string `json:"answer_token"`
} `json:"data"`
}
Loading

0 comments on commit a9f7dc3

Please sign in to comment.