func (db *repoStore) Get(ID int64) (*model.Repo, error) { repo := new(model.Repo) err := meddler.Load(db, repoTable, repo, ID) if err != nil { return nil, err } return repo, nil }
// PutProblemBundle handles a request to /v2/problem_bundles/:problem_id, // updating an existing problem. // The bundle must have a full set of passing commits signed by the daycare. // If any assignments exist that refer to this problem, then the updates cannot change the number // of steps in the problem. func PutProblemBundle(w http.ResponseWriter, tx *sql.Tx, params martini.Params, bundle ProblemBundle, render render.Render) { if bundle.Problem == nil { loggedHTTPErrorf(w, http.StatusBadRequest, "bundle must contain a problem") return } if bundle.Problem.ID <= 0 { loggedHTTPErrorf(w, http.StatusBadRequest, "updated problem must have ID > 0") return } old := new(Problem) if err := meddler.Load(tx, "problems", old, bundle.Problem.ID); err != nil { loggedHTTPDBNotFoundError(w, err) return } if bundle.Problem.Unique != old.Unique { loggedHTTPErrorf(w, http.StatusBadRequest, "updating a problem cannot change its unique ID from %q to %q; create a new problem instead", old.Unique, bundle.Problem.Unique) return } if bundle.Problem.ProblemType != old.ProblemType { loggedHTTPErrorf(w, http.StatusBadRequest, "updating a problem cannot change its type from %q to %q; create a new problem instead", old.ProblemType, bundle.Problem.ProblemType) return } if !bundle.Problem.CreatedAt.Equal(old.CreatedAt) { loggedHTTPErrorf(w, http.StatusBadRequest, "updating a problem cannot change its created time from %v to %v", old.CreatedAt, bundle.Problem.CreatedAt) return } var assignmentCount int if err := tx.QueryRow(`SELECT COUNT(1) FROM assignments INNER JOIN problem_sets ON assignments.problem_set_id = problem_sets.id INNER JOIN problem_set_problems ON problem_sets.id = problem_set_problems.problem_set_id WHERE problem_set_problems.problem_id = $1`, bundle.Problem.ID).Scan(&assignmentCount); err != nil { loggedHTTPErrorf(w, http.StatusInternalServerError, "db error: %v", err) return } if assignmentCount > 0 { // count the steps in the old problem var stepCount int if err := tx.QueryRow(`SELECT COUNT(1) FROM problem_steps WHERE problem_id = $1`, bundle.Problem.ID).Scan(&stepCount); err != nil { loggedHTTPErrorf(w, http.StatusInternalServerError, "db error: %v", err) return } if len(bundle.ProblemSteps) != stepCount { loggedHTTPErrorf(w, http.StatusBadRequest, "cannot change the number of steps in a problem that is already in use") return } } saveProblemBundleCommon(w, tx, &bundle, render) }
func doMoneyTest(t *testing.T, curr string, amt string, expectedIso int16, expectedAmount int64) { c := NewTestConfig(t) ctx, err := NewContext(c) if err != nil { t.Fatal(err) } defer ctx.Close() _, err = ctx.DB.Exec("CREATE TEMPORARY TABLE money_test (id BIGSERIAL PRIMARY KEY, cash monetary)") if err != nil { t.Fatal(err) } cash, err := NewMonetary(ctx.DB, curr, amt) if err != nil { if expectedIso == 0 && expectedAmount == 0 { return } t.Fatal(err) } m := moneyTest{0, cash} err = meddler.Insert(ctx.DB, "money_test", &m) if err != nil { t.Fatal(err) } m2 := moneyTest{} err = meddler.Load(ctx.DB, "money_test", &m2, m.Id) if err != nil { t.Fatal(err) } if m2.Cash.Iso4217Code != expectedIso { t.Fatal("ISO code incorrect") } if m2.Cash.Amount != expectedAmount { t.Fatal("Amount incorrect") } }
// GetCourse handles /v2/courses/:course_id requests, // returning a single course. func GetCourse(w http.ResponseWriter, tx *sql.Tx, params martini.Params, currentUser *User, render render.Render) { courseID, err := parseID(w, "course_id", params["course_id"]) if err != nil { return } course := new(Course) if currentUser.Admin { err = meddler.Load(tx, "courses", course, courseID) } else { err = meddler.QueryRow(tx, course, `SELECT courses.* `+ `FROM courses JOIN assignments ON courses.id = assignments.course_id `+ `WHERE assignments.user_id = $1 AND assignments.course_id = $2`, currentUser.ID, courseID) } if err != nil { loggedHTTPDBNotFoundError(w, err) return } render.JSON(http.StatusOK, course) }
// GetUser handles /v2/users/:user_id requests, // returning a single user. func GetUser(w http.ResponseWriter, tx *sql.Tx, params martini.Params, currentUser *User, render render.Render) { userID, err := parseID(w, "user_id", params["user_id"]) if err != nil { return } user := new(User) if currentUser.Admin { err = meddler.Load(tx, "users", user, int64(userID)) } else { err = meddler.QueryRow(tx, &user, `SELECT users.* `+ `FROM users JOIN user_users ON users.id = user_users.other_user_id `+ `WHERE user_users.user_id = $1 AND user_users.other_user_id = $2`, currentUser.ID, userID) } if err != nil { loggedHTTPDBNotFoundError(w, err) return } render.JSON(http.StatusOK, user) }
// GetProblem handles a request to /v2/problems/:problem_id, // returning a single problem. func GetProblem(w http.ResponseWriter, tx *sql.Tx, params martini.Params, currentUser *User, render render.Render) { problemID, err := parseID(w, "problem_id", params["problem_id"]) if err != nil { return } problem := new(Problem) if currentUser.Admin || currentUser.Author { err = meddler.Load(tx, "problems", problem, problemID) } else { err = meddler.QueryRow(tx, problem, `SELECT problems.* `+ `FROM problems JOIN user_problems ON problems.id = problem_id `+ `WHERE user_id = $1 AND problem_id = $2`, currentUser.ID, problemID) } if err != nil { loggedHTTPDBNotFoundError(w, err) return } render.JSON(http.StatusOK, problem) }
// PostProblemBundleUnconfirmed handles a request to /v2/problem_bundles/unconfirmed, // signing a new/updated problem that has not yet been tested on the daycare. func PostProblemBundleUnconfirmed(w http.ResponseWriter, tx *sql.Tx, currentUser *User, bundle ProblemBundle, render render.Render) { now := time.Now() // basic sanity checks if len(bundle.ProblemSteps) < 2 { loggedHTTPErrorf(w, http.StatusBadRequest, "problem must have at least one step") return } if len(bundle.ProblemSteps) != len(bundle.Commits) { loggedHTTPErrorf(w, http.StatusBadRequest, "problem must have exactly one commit for each step") return } if len(bundle.ProblemSignature) != 0 { loggedHTTPErrorf(w, http.StatusBadRequest, "unconfirmed bundle must not have problem signature") } if len(bundle.CommitSignatures) != 0 { loggedHTTPErrorf(w, http.StatusBadRequest, "unconfirmed bundle must not have commit signatures") } // clean up basic fields and do some checks if err := bundle.Problem.Normalize(now, bundle.ProblemSteps); err != nil { loggedHTTPErrorf(w, http.StatusBadRequest, "%v", err) return } // if this is an update to an existing problem, we need to check that some things match if bundle.Problem.ID != 0 { old := new(Problem) if err := meddler.Load(tx, "problems", old, int64(bundle.Problem.ID)); err != nil { if err == sql.ErrNoRows { loggedHTTPErrorf(w, http.StatusNotFound, "request to update problem %d, but that problem does not exist", bundle.Problem.ID) } else { loggedHTTPErrorf(w, http.StatusInternalServerError, "db error: %v", err) } return } if bundle.Problem.Unique != old.Unique { loggedHTTPErrorf(w, http.StatusBadRequest, "updating a problem cannot change its unique ID from %q to %q; create a new problem instead", old.Unique, bundle.Problem.Unique) return } if bundle.Problem.ProblemType != old.ProblemType { loggedHTTPErrorf(w, http.StatusBadRequest, "updating a problem cannot change its type from %q to %q; create a new problem instead", old.ProblemType, bundle.Problem.ProblemType) return } if !bundle.Problem.CreatedAt.Equal(old.CreatedAt) { loggedHTTPErrorf(w, http.StatusBadRequest, "updating a problem cannot change its created time from %v to %v", old.CreatedAt, bundle.Problem.CreatedAt) return } } else { // for new problems, set the created timestamp to now bundle.Problem.CreatedAt = now } // make sure the unique ID is unique conflict := new(Problem) if err := meddler.QueryRow(tx, conflict, `SELECT * FROM problems WHERE unique_id = $1`, bundle.Problem.Unique); err != nil { if err == sql.ErrNoRows { conflict.ID = 0 } else { loggedHTTPErrorf(w, http.StatusInternalServerError, "db error: %v", err) return } } if conflict.ID != 0 && conflict.ID != bundle.Problem.ID { loggedHTTPErrorf(w, http.StatusBadRequest, "unique ID %q is already in use by problem %d", bundle.Problem.Unique, conflict.ID) return } // update the timestamp bundle.Problem.UpdatedAt = now // compute signature bundle.ProblemSignature = bundle.Problem.ComputeSignature(Config.DaycareSecret, bundle.ProblemSteps) // check the commits whitelists := bundle.Problem.GetStepWhitelists(bundle.ProblemSteps) bundle.CommitSignatures = nil for n, commit := range bundle.Commits { commit.ID = 0 commit.AssignmentID = 0 commit.ProblemID = bundle.Problem.ID commit.Step = int64(n) + 1 if commit.Action != "confirm" { loggedHTTPErrorf(w, http.StatusBadRequest, "commit %d has action %q, expected %q", n, commit.Action, "confirm") return } commit.Transcript = []*EventMessage{} commit.ReportCard = nil commit.Score = 0.0 commit.CreatedAt = now commit.UpdatedAt = now if err := commit.Normalize(now, whitelists[n]); err != nil { loggedHTTPErrorf(w, http.StatusBadRequest, "commit %d: %v", n, err) return } // set timestamps and compute signature sig := commit.ComputeSignature(Config.DaycareSecret, bundle.ProblemSignature) bundle.CommitSignatures = append(bundle.CommitSignatures, sig) } render.JSON(http.StatusOK, &bundle) }
func (db *nodestore) Get(id int64) (*model.Node, error) { var node = new(model.Node) var err = meddler.Load(db, nodeTable, node, id) return node, err }
// GetRepo retrieves a specific repo from the // datastore for the given ID. func (db *Repostore) GetRepo(id int64) (*model.Repo, error) { var repo = new(model.Repo) var err = meddler.Load(db, repoTable, repo, id) return repo, err }
func (db *userStore) Get(id int64) (*model.User, error) { user := new(model.User) err := meddler.Load(db, userTable, user, id) return user, err }
func main() { // parse command line var configFile string flag.StringVar(&configFile, "config", "/etc/codegrinder/config.json", "Path to the config file") var ta, daycare bool flag.BoolVar(&ta, "ta", true, "Serve the TA role") flag.BoolVar(&daycare, "daycare", true, "Serve the daycare role") flag.Parse() if !ta && !daycare { log.Fatalf("must run at least one role (ta/daycare)") } // set config defaults Config.ToolName = "CodeGrinder" Config.ToolID = "codegrinder" Config.ToolDescription = "Programming exercises with grading" Config.LetsEncryptCache = "/etc/codegrinder/letsencrypt.cache" Config.PostgresHost = "/var/run/postgresql" Config.PostgresPort = "" Config.PostgresUsername = os.Getenv("USER") Config.PostgresPassword = "" Config.PostgresDatabase = os.Getenv("USER") // load config file if raw, err := ioutil.ReadFile(configFile); err != nil { log.Fatalf("failed to load config file %q: %v", configFile, err) } else if err := json.Unmarshal(raw, &Config); err != nil { log.Fatalf("failed to parse config file: %v", err) } Config.SessionSecret = unBase64(Config.SessionSecret) Config.DaycareSecret = unBase64(Config.DaycareSecret) // set up martini r := martini.NewRouter() m := martini.New() m.Logger(log.New(os.Stderr, "", log.LstdFlags)) m.Use(martini.Logger()) m.Use(martini.Recovery()) m.Use(martini.Static(Config.StaticDir, martini.StaticOptions{SkipLogging: true})) m.MapTo(r, (*martini.Routes)(nil)) m.Action(r.Handle) m.Use(render.Renderer(render.Options{IndentJSON: true})) store := sessions.NewCookieStore([]byte(Config.SessionSecret)) m.Use(sessions.Sessions(CookieName, store)) // sessions expire June 30 and December 31 go func() { for { now := time.Now() // expire at the end of the calendar year expires := time.Date(now.Year(), time.December, 31, 23, 59, 59, 0, time.Local) if expires.Sub(now).Hours() < 14*24 { // are we within 2 weeks of the end of the year? probably prepping for spring, // so expire next June 30 instead expires = time.Date(now.Year()+1, time.June, 30, 23, 59, 59, 0, time.Local) } else if expires.Sub(now).Hours() > (365/2+14)*24 { // is it still more than 2 weeks before June 30? probably in spring semester, // so expire this June 30 instead expires = time.Date(now.Year(), time.June, 30, 23, 59, 59, 0, time.Local) } store.Options(sessions.Options{Path: "/", Secure: true, MaxAge: int(expires.Sub(now).Seconds())}) time.Sleep(11 * time.Minute) } }() // set up TA role if ta { // make sure relevant secrets are included in config file if Config.LTISecret == "" { log.Fatalf("cannot run TA role with no LTISecret in the config file") } if Config.SessionSecret == "" { log.Fatalf("cannot run TA role with no SessionSecret in the config file") } if Config.DaycareSecret == "" { log.Fatalf("cannot run with no DaycareSecret in the config file") } // set up the database db := setupDB(Config.PostgresHost, Config.PostgresPort, Config.PostgresUsername, Config.PostgresPassword, Config.PostgresDatabase) // martini service: wrap handler in a transaction withTx := func(c martini.Context, w http.ResponseWriter) { // start a transaction tx, err := db.Begin() if err != nil { loggedHTTPErrorf(w, http.StatusInternalServerError, "db error starting transaction: %v", err) return } // pass it on to the main handler c.Map(tx) c.Next() // was it a successful result? rw := w.(martini.ResponseWriter) if rw.Status() < http.StatusBadRequest { // commit the transaction if err := tx.Commit(); err != nil { loggedHTTPErrorf(w, http.StatusInternalServerError, "db error committing transaction: %v", err) return } } else { // rollback log.Printf("rolling back transaction") if err := tx.Rollback(); err != nil { loggedHTTPErrorf(w, http.StatusInternalServerError, "db error rolling back transaction: %v", err) return } } } // martini service: to require an active logged-in session auth := func(w http.ResponseWriter, session sessions.Session) { if userID := session.Get("id"); userID == nil { loggedHTTPErrorf(w, http.StatusUnauthorized, "authentication: no user ID found in session") return } } // martini service: include the current logged-in user (requires withTx and auth) withCurrentUser := func(c martini.Context, w http.ResponseWriter, tx *sql.Tx, session sessions.Session) { rawID := session.Get("id") if rawID == nil { loggedHTTPErrorf(w, http.StatusInternalServerError, "cannot find user ID in session") return } userID, ok := rawID.(int64) if !ok { session.Clear() loggedHTTPErrorf(w, http.StatusInternalServerError, "error extracting user ID from session") return } // load the user record user := new(User) if err := meddler.Load(tx, "users", user, userID); err != nil { if err == sql.ErrNoRows { loggedHTTPErrorf(w, http.StatusUnauthorized, "user %d not found", userID) return } loggedHTTPErrorf(w, http.StatusInternalServerError, "db error: %v", err) return } // map the current user to the request context c.Map(user) } // martini service: require logged in user to be an administrator (requires withCurrentUser) administratorOnly := func(w http.ResponseWriter, currentUser *User) { if !currentUser.Admin { loggedHTTPErrorf(w, http.StatusUnauthorized, "user %d (%s) is not an administrator", currentUser.ID, currentUser.Email) return } } // martini service: require logged in user to be an author or administrator (requires withCurrentUser) authorOnly := func(w http.ResponseWriter, tx *sql.Tx, currentUser *User) { if currentUser.Admin { return } if !currentUser.Author { loggedHTTPErrorf(w, http.StatusUnauthorized, "user %d (%s) is not an author", currentUser.ID, currentUser.Name) return } } // version r.Get("/v2/version", func(w http.ResponseWriter, render render.Render) { render.JSON(http.StatusOK, &CurrentVersion) }) // LTI r.Get("/v2/lti/config.xml", GetConfigXML) r.Post("/v2/lti/problem_sets", binding.Bind(LTIRequest{}), checkOAuthSignature, withTx, LtiProblemSets) r.Post("/v2/lti/problem_sets/:unique", binding.Bind(LTIRequest{}), checkOAuthSignature, withTx, LtiProblemSet) // problem bundles--for problem creation only r.Post("/v2/problem_bundles/unconfirmed", auth, withTx, withCurrentUser, authorOnly, binding.Json(ProblemBundle{}), PostProblemBundleUnconfirmed) r.Post("/v2/problem_bundles/confirmed", auth, withTx, withCurrentUser, authorOnly, binding.Json(ProblemBundle{}), PostProblemBundleConfirmed) r.Put("/v2/problem_bundles/:problem_id", auth, withTx, withCurrentUser, authorOnly, binding.Json(ProblemBundle{}), PutProblemBundle) // problem set bundles--for problem set creation only r.Post("/v2/problem_set_bundles", auth, withTx, withCurrentUser, authorOnly, binding.Json(ProblemSetBundle{}), PostProblemSetBundle) // problem types r.Get("/v2/problem_types", auth, GetProblemTypes) r.Get("/v2/problem_types/:name", auth, GetProblemType) // problems r.Get("/v2/problems", auth, withTx, withCurrentUser, GetProblems) r.Get("/v2/problems/:problem_id", auth, withTx, withCurrentUser, GetProblem) r.Get("/v2/problems/:problem_id/steps", auth, withTx, withCurrentUser, GetProblemSteps) r.Get("/v2/problems/:problem_id/steps/:step", auth, withTx, withCurrentUser, GetProblemStep) r.Delete("/v2/problems/:problem_id", auth, withTx, withCurrentUser, administratorOnly, DeleteProblem) // problem sets r.Get("/v2/problem_sets", auth, withTx, withCurrentUser, GetProblemSets) r.Get("/v2/problem_sets/:problem_set_id", auth, withTx, withCurrentUser, GetProblemSet) r.Get("/v2/problem_sets/:problem_set_id/problems", auth, withTx, withCurrentUser, GetProblemSetProblems) r.Delete("/v2/problem_sets/:problem_set_id", auth, withTx, withCurrentUser, administratorOnly, DeleteProblemSet) // courses r.Get("/v2/courses", auth, withTx, withCurrentUser, GetCourses) r.Get("/v2/courses/:course_id", auth, withTx, withCurrentUser, GetCourse) r.Delete("/v2/courses/:course_id", auth, withTx, withCurrentUser, administratorOnly, DeleteCourse) // users r.Get("/v2/users", auth, withTx, withCurrentUser, GetUsers) r.Get("/v2/users/me", auth, withTx, withCurrentUser, GetUserMe) r.Get("/v2/users/me/cookie", auth, GetUserMeCookie) r.Get("/v2/users/:user_id", auth, withTx, withCurrentUser, GetUser) r.Get("/v2/courses/:course_id/users", auth, withTx, withCurrentUser, GetCourseUsers) r.Delete("/v2/users/:user_id", auth, withTx, withCurrentUser, administratorOnly, DeleteUser) // assignments r.Get("/v2/users/:user_id/assignments", auth, withTx, withCurrentUser, GetUserAssignments) r.Get("/v2/courses/:course_id/users/:user_id/assignments", auth, withTx, withCurrentUser, GetCourseUserAssignments) r.Get("/v2/assignments/:assignment_id", auth, withTx, withCurrentUser, GetAssignment) r.Delete("/v2/assignments/:assignment_id", auth, withTx, withCurrentUser, administratorOnly, DeleteAssignment) // commits r.Get("/v2/assignments/:assignment_id/problems/:problem_id/commits/last", auth, withTx, withCurrentUser, GetAssignmentProblemCommitLast) r.Get("/v2/assignments/:assignment_id/problems/:problem_id/steps/:step/commits/last", auth, withTx, withCurrentUser, GetAssignmentProblemStepCommitLast) r.Delete("/v2/commits/:commit_id", auth, withTx, withCurrentUser, administratorOnly, DeleteCommit) // commit bundles r.Post("/v2/commit_bundles/unsigned", auth, withTx, withCurrentUser, binding.Json(CommitBundle{}), PostCommitBundlesUnsigned) r.Post("/v2/commit_bundles/signed", auth, withTx, withCurrentUser, binding.Json(CommitBundle{}), PostCommitBundlesSigned) } // set up daycare role if daycare { // make sure relevant secrets are included in config file if Config.DaycareSecret == "" { log.Fatalf("cannot run with no DaycareSecret in the config file") } // attach to docker and try a ping var err error dockerClient, err = docker.NewVersionedClient("unix:///var/run/docker.sock", "1.18") if err != nil { log.Fatalf("NewVersionedClient: %v", err) } if err = dockerClient.Ping(); err != nil { log.Fatalf("Ping: %v", err) } r.Get("/v2/sockets/:problem_type/:action", SocketProblemTypeAction) } // start redirecting http calls to https log.Printf("starting http -> https forwarder") go http.ListenAndServe(":http", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { // get the address of the client addr := r.Header.Get("X-Real-IP") if addr == "" { addr = r.Header.Get("X-Forwarded-For") if addr == "" { addr = r.RemoteAddr } } // make sure the request is for the right host name if Config.Hostname != r.Host { loggedHTTPErrorf(w, http.StatusNotFound, "http request to invalid host: %s", r.Host) return } var u url.URL = *r.URL u.Scheme = "https" u.Host = Config.Hostname log.Printf("redirecting http request from %s to %s", addr, u.String()) http.Redirect(w, r, u.String(), http.StatusMovedPermanently) })) // set up letsencrypt lem := letsencrypt.Manager{} if err := lem.CacheFile(Config.LetsEncryptCache); err != nil { log.Fatalf("Setting up LetsEncrypt: %v", err) } lem.SetHosts([]string{Config.Hostname}) if !lem.Registered() { log.Printf("registering with letsencrypt") if err := lem.Register(Config.LetsEncryptEmail, nil); err != nil { log.Fatalf("Registering with LetsEncrypt: %v", err) } } // start the https server log.Printf("accepting https connections") server := &http.Server{ Addr: ":https", Handler: m, TLSConfig: &tls.Config{ MinVersion: tls.VersionTLS10, GetCertificate: lem.GetCertificate, }, } if err := server.ListenAndServeTLS("", ""); err != nil { log.Fatalf("ListenAndServeTLS: %v", err) } }
func GetExpenseById(db meddler.DB, id int64) (*Expense, error) { expense := new(Expense) err := meddler.Load(db, "expenses", expense, id) return expense, err }
func GetTransactionById(db meddler.DB, id int64) (*Transaction, error) { trans := new(Transaction) err := meddler.Load(db, "transactions", trans, id) return trans, err }
// GetCommit retrieves a commit from the // datastore for the given ID. func (db *Commitstore) GetCommit(id int64) (*model.Commit, error) { var commit = new(model.Commit) var err = meddler.Load(db, commitTable, commit, id) return commit, err }
func GetUserById(db meddler.DB, id int64) (*User, error) { user := new(User) err := meddler.Load(db, "users", user, id) return user, err }
func (db *datastore) GetUser(id int64) (*model.User, error) { var usr = new(model.User) var err = meddler.Load(db, userTable, usr, id) return usr, err }
func (u *User) Reload(db meddler.DB) error { return meddler.Load(db, "users", u, u.Id) }
func (db *jobstore) Get(id int64) (*model.Job, error) { var job = new(model.Job) var err = meddler.Load(db, jobTable, job, id) return job, err }
func GetLabelById(db meddler.DB, id int64) (*Label, error) { label := new(Label) err := meddler.Load(db, "labels", label, id) return label, err }
func (db *buildstore) Get(id int64) (*model.Build, error) { var build = new(model.Build) var err = meddler.Load(db, buildTable, build, id) return build, err }
func (db *datastore) GetAgent(id int64) (*model.Agent, error) { var agent = new(model.Agent) var err = meddler.Load(db, agentTable, agent, id) return agent, err }