From d02f3f9951ef40f2a96cec4ae9f24055844722cc Mon Sep 17 00:00:00 2001 From: Howl Date: Tue, 5 Apr 2016 22:22:13 +0200 Subject: [PATCH] Add token creation (login) --- app/start.go | 2 + app/tokens.go | 4 +- app/v1/errors.go | 16 ++++- app/v1/token.go | 134 ++++++++++++++++++++++++++++++++++++++++ common/privileges.go | 28 +++++++++ common/random_string.go | 33 ++++++++++ 6 files changed, 215 insertions(+), 2 deletions(-) create mode 100644 app/v1/token.go create mode 100644 common/random_string.go diff --git a/app/start.go b/app/start.go index 1ef403e..4f5a46e 100644 --- a/app/start.go +++ b/app/start.go @@ -18,6 +18,8 @@ func Start(conf common.Conf, db *sql.DB) { { gv1 := api.Group("/v1") { + gv1.POST("/token/new", Method(v1.TokenNewPOST, db)) + // Auth-free API endpoints gv1.GET("/ping", Method(v1.PingGET, db)) gv1.GET("/surprise_me", Method(v1.SurpriseMeGET, db)) diff --git a/app/tokens.go b/app/tokens.go index 5b5a2c1..de4fb2c 100644 --- a/app/tokens.go +++ b/app/tokens.go @@ -1,7 +1,9 @@ package app import ( + "crypto/md5" "database/sql" + "fmt" "github.com/osuripple/api/common" ) @@ -10,7 +12,7 @@ import ( func GetTokenFull(token string, db *sql.DB) (common.Token, bool) { var uid int var privs int - err := db.QueryRow("SELECT user, privileges FROM tokens WHERE token = ? LIMIT 1", token).Scan(&uid, &privs) + err := db.QueryRow("SELECT user, privileges FROM tokens WHERE token = ? LIMIT 1", fmt.Sprintf("%x", md5.Sum([]byte(token)))).Scan(&uid, &privs) switch { case err == sql.ErrNoRows: return common.Token{}, false diff --git a/app/v1/errors.go b/app/v1/errors.go index bf5965f..4d7fabe 100644 --- a/app/v1/errors.go +++ b/app/v1/errors.go @@ -1,13 +1,27 @@ package v1 import ( + "strings" + "github.com/osuripple/api/common" ) // Boilerplate errors var ( Err500 = common.Response{ - Code: 0, + Code: 500, Message: "An error occurred. Try again, perhaps?", } + ErrBadJSON = common.Response{ + Code: 400, + Message: "There was an error processing your JSON data.", + } ) + +// ErrMissingField generates a response to a request when some fields in the JSON are missing. +func ErrMissingField(missingFields ...string) common.Response { + return common.Response{ + Code: 422, // http://stackoverflow.com/a/10323055/5328069 + Message: "Missing fields: " + strings.Join(missingFields, ", ") + ".", + } +} diff --git a/app/v1/token.go b/app/v1/token.go new file mode 100644 index 0000000..c5f49d7 --- /dev/null +++ b/app/v1/token.go @@ -0,0 +1,134 @@ +package v1 + +import ( + "crypto/md5" + "database/sql" + "encoding/json" + "fmt" + + "github.com/osuripple/api/common" + "golang.org/x/crypto/bcrypt" +) + +type tokenNewInData struct { + // either username or userid must be given in the request. + // if none is given, the request is trashed. + Username string `json:"username"` + UserID int `json:"id"` + Password string `json:"password"` + Privileges int `json:"privileges"` + Description string `json:"description"` +} + +type tokenNewOutData struct { + Username string `json:"username"` + ID int `json:"id"` + Privileges int `json:"privileges"` + Token string `json:"token,omitempty"` + Banned bool `json:"banned"` +} + +// TokenNewPOST is the handler for POST /token/new. +func TokenNewPOST(md common.MethodData) (r common.Response) { + data := tokenNewInData{} + err := json.Unmarshal(md.RequestData, &data) + if err != nil { + r = ErrBadJSON + return + } + + var miss []string + if data.Username == "" && data.UserID == 0 { + miss = append(miss, "username|id") + } + if data.Password == "" { + miss = append(miss, "password") + } + if len(miss) != 0 { + r = ErrMissingField(miss...) + return + } + + var q *sql.Row + const base = "SELECT id, username, rank, password_md5, password_version, allowed FROM users " + if data.UserID != 0 { + q = md.DB.QueryRow(base+"WHERE id = ? LIMIT 1", data.UserID) + } else { + q = md.DB.QueryRow(base+"WHERE username = ? LIMIT 1", data.Username) + } + + ret := tokenNewOutData{} + var ( + rank int + pw string + pwVersion int + allowed int + ) + + err = q.Scan(&ret.ID, &ret.Username, &rank, &pw, &pwVersion, &allowed) + switch { + case err == sql.ErrNoRows: + r.Code = 404 + r.Message = "No user with that username/id was found." + return + case err != nil: + md.C.Error(err) + r = Err500 + return + } + + if pwVersion == 1 { + r.Code = 418 // Teapots! + r.Message = "That user still has a password in version 1. Unfortunately, in order for the API to check for the password to be OK, the user has to first log in through the website." + return + } + if err := bcrypt.CompareHashAndPassword([]byte(pw), []byte(fmt.Sprintf("%x", md5.Sum([]byte(data.Password))))); err != nil { + if err == bcrypt.ErrMismatchedHashAndPassword { + r.Code = 403 + r.Message = "That password doesn't match!" + return + } + md.C.Error(err) + r = Err500 + return + } + if allowed == 0 { + r.Code = 200 + r.Message = "That user is banned." + ret.Banned = true + r.Data = ret + return + } + ret.Privileges = int(common.Privileges(data.Privileges).CanOnly(rank)) + + var ( + tokenStr string + tokenMD5 string + ) + for { + tokenStr = common.RandomString(32) + tokenMD5 = fmt.Sprintf("%x", md5.Sum([]byte(tokenStr))) + ret.Token = tokenStr + id := 0 + + err := md.DB.QueryRow("SELECT id FROM tokens WHERE token=?", tokenMD5).Scan(&id) + if err == sql.ErrNoRows { + break + } + if err != nil { + md.C.Error(err) + r = Err500 + return + } + } + _, err = md.DB.Exec("INSERT INTO tokens(user, privileges, description, token) VALUES (?, ?, ?, ?)", ret.ID, ret.Privileges, data.Description, tokenMD5) + if err != nil { + md.C.Error(err) + r = Err500 + return + } + + r.Code = 200 + r.Data = ret + return +} diff --git a/common/privileges.go b/common/privileges.go index 06bab1a..47e19fc 100644 --- a/common/privileges.go +++ b/common/privileges.go @@ -98,3 +98,31 @@ func (p Privileges) String() string { } return strings.Join(pvs, ", ") } + +var privilegeMustBe = [...]int{ + 1, + 1, + 1, + 3, + 3, + 4, + 4, + 4, + 4, + 4, + 3, +} + +// CanOnly removes any privilege that the user has requested to have, but cannot have due to their rank. +func (p Privileges) CanOnly(rank int) Privileges { + newPrivilege := 0 + for i, v := range privilegeMustBe { + wants := p&1 == 1 + can := rank >= v + if wants && can { + newPrivilege |= 1 << uint(i) + } + p >>= 1 + } + return Privileges(newPrivilege) +} diff --git a/common/random_string.go b/common/random_string.go new file mode 100644 index 0000000..43d6ce5 --- /dev/null +++ b/common/random_string.go @@ -0,0 +1,33 @@ +package common + +import ( + "math/rand" + "time" +) + +const letterBytes = "0123456789abcdef" +const ( + letterIdxBits = 6 // 6 bits to represent a letter index + letterIdxMask = 1<= 0; { + if remain == 0 { + cache, remain = randSrc.Int63(), letterIdxMax + } + if idx := int(cache & letterIdxMask); idx < len(letterBytes) { + b[i] = letterBytes[idx] + i-- + } + cache >>= letterIdxBits + remain-- + } + return string(b) +}