diff --git a/app/method.go b/app/method.go index 71afeef..7af3908 100644 --- a/app/method.go +++ b/app/method.go @@ -75,7 +75,7 @@ func initialCaretaker(c *gin.Context, f func(md common.MethodData) common.CodeMe missingPrivileges := 0 for _, privilege := range privilegesNeeded { - if int(md.User.Privileges)&privilege == 0 { + if uint64(md.User.TokenPrivileges)&uint64(privilege) == 0 { missingPrivileges |= privilege } } @@ -95,13 +95,13 @@ func initialCaretaker(c *gin.Context, f func(md common.MethodData) common.CodeMe } } - if _, exists := c.GetQuery("pls200"); exists { + if md.HasQuery("pls200") { c.Writer.WriteHeader(200) } else { c.Writer.WriteHeader(resp.GetCode()) } - if _, exists := c.GetQuery("callback"); exists { + if md.HasQuery("callback") { c.Header("Content-Type", "application/javascript; charset=utf-8") } else { c.Header("Content-Type", "application/json; charset=utf-8") diff --git a/app/start.go b/app/start.go index 93d02fb..b5c82f1 100644 --- a/app/start.go +++ b/app/start.go @@ -62,7 +62,6 @@ func Start(conf common.Conf, dbO *sqlx.DB) *gin.Engine { // Auth-free API endpoints (public data) gv1.GET("/ping", Method(v1.PingGET)) gv1.GET("/surprise_me", Method(v1.SurpriseMeGET)) - gv1.GET("/privileges", Method(v1.PrivilegesGET)) gv1.GET("/doc", Method(v1.DocGET)) gv1.GET("/doc/content", Method(v1.DocContentGET)) gv1.GET("/doc/rules", Method(v1.DocRulesGET)) diff --git a/app/tokens.go b/app/tokens.go index fa297d1..25463c9 100644 --- a/app/tokens.go +++ b/app/tokens.go @@ -13,17 +13,25 @@ import ( // GetTokenFull retrieves an user ID and their token privileges knowing their API token. func GetTokenFull(token string, db *sqlx.DB) (common.Token, bool) { var t common.Token - var privs uint64 + var ( + tokenPrivsRaw uint64 + userPrivsRaw uint64 + ) var priv8 bool - err := db.QueryRow("SELECT id, user, privileges, private FROM tokens WHERE token = ? LIMIT 1", + err := db.QueryRow(`SELECT + t.id, t.user, t.privileges, t.private, u.privileges +FROM tokens t +LEFT JOIN users u ON u.id = t.user +WHERE token = ? LIMIT 1`, fmt.Sprintf("%x", md5.Sum([]byte(token)))). Scan( - &t.ID, &t.UserID, &privs, &priv8, + &t.ID, &t.UserID, &tokenPrivsRaw, &priv8, &userPrivsRaw, ) if priv8 { - privs = common.PrivilegeReadConfidential | common.PrivilegeWrite + tokenPrivsRaw = common.PrivilegeReadConfidential | common.PrivilegeWrite } - t.Privileges = common.Privileges(privs) + t.TokenPrivileges = common.Privileges(tokenPrivsRaw) + t.UserPrivileges = common.UserPrivileges(userPrivsRaw) switch { case err == sql.ErrNoRows: return common.Token{}, false diff --git a/app/v1/blog.go b/app/v1/blog.go index 1b531c1..c6d6d4b 100644 --- a/app/v1/blog.go +++ b/app/v1/blog.go @@ -71,7 +71,7 @@ type blogPostContent struct { // BlogPostsContentGET retrieves the content of a specific blog post. func BlogPostsContentGET(md common.MethodData) common.CodeMessager { field := "markdown" - if _, present := md.C.GetQuery("html"); present { + if md.HasQuery("html") { field = "html" } var ( diff --git a/app/v1/doc.go b/app/v1/doc.go index 734142c..49da85d 100644 --- a/app/v1/doc.go +++ b/app/v1/doc.go @@ -21,7 +21,7 @@ type docResponse struct { // DocGET retrieves a list of documentation files. func DocGET(md common.MethodData) common.CodeMessager { var wc string - if !md.User.Privileges.HasPrivilegeBlog() || md.Query("public") == "1" { + if md.User.TokenPrivileges&common.PrivilegeBlog == 0 || md.Query("public") == "1" { wc = "WHERE public = '1'" } rows, err := md.DB.Query("SELECT id, doc_name, public, is_rule FROM docs " + wc) @@ -55,7 +55,7 @@ func DocContentGET(md common.MethodData) common.CodeMessager { return common.SimpleResponse(404, "Documentation file not found!") } var wc string - if !md.User.Privileges.HasPrivilegeBlog() || md.Query("public") == "1" { + if md.User.TokenPrivileges&common.PrivilegeBlog == 0 || md.Query("public") == "1" { wc = "AND public = '1'" } var r docContentResponse diff --git a/app/v1/friend.go b/app/v1/friend.go index 386378b..6a0e950 100644 --- a/app/v1/friend.go +++ b/app/v1/friend.go @@ -157,7 +157,8 @@ func addFriend(md common.MethodData, u int) common.CodeMessager { // userExists makes sure an user exists. func userExists(md common.MethodData, u int) (r bool) { - err := md.DB.QueryRow("SELECT EXISTS(SELECT 1 FROM users WHERE id = ? AND users.privileges & 1 > 0)", u).Scan(&r) + err := md.DB.QueryRow("SELECT EXISTS(SELECT 1 FROM users WHERE id = ? AND "+ + md.User.OnlyUserPublic(true)+")", u).Scan(&r) if err != nil && err != sql.ErrNoRows { md.Err(err) } diff --git a/app/v1/leaderboard.go b/app/v1/leaderboard.go index 1f881a0..340c3bb 100644 --- a/app/v1/leaderboard.go +++ b/app/v1/leaderboard.go @@ -36,8 +36,10 @@ INNER JOIN users_stats ON users_stats.id = leaderboard_%[1]s.user // LeaderboardGET gets the leaderboard. func LeaderboardGET(md common.MethodData) common.CodeMessager { m := getMode(md.Query("mode")) - query := fmt.Sprintf(lbUserQuery, m, `WHERE users.privileges & 1 > 0 ORDER BY leaderboard_`+m+`.position `+ - common.Paginate(md.Query("p"), md.Query("l"), 100)) + // Admins may not want to see banned users on the leaderboard. + // This is the default setting. In case they do, they have to activate see_everything. + query := fmt.Sprintf(lbUserQuery, m, `WHERE `+md.User.OnlyUserPublic(md.HasQuery("see_everything"))+ + ` ORDER BY leaderboard_`+m+`.position `+common.Paginate(md.Query("p"), md.Query("l"), 100)) rows, err := md.DB.Query(query) if err != nil { md.Err(err) diff --git a/app/v1/manage_user.go b/app/v1/manage_user.go index dea9d9a..637f641 100644 --- a/app/v1/manage_user.go +++ b/app/v1/manage_user.go @@ -21,21 +21,15 @@ func UserManageSetAllowedPOST(md common.MethodData) common.CodeMessager { return common.SimpleResponse(400, "Allowed status must be between 0 and 2") } var banDatetime int64 - var privileges int32 - var newPrivileges int32 - err := md.DB.QueryRow("SELECT privileges FROM users WHERE id = ?", data.UserID).Scan(&privileges) - if err != nil { - md.Err(err) - return Err500 - } + var privsSet string if data.Allowed == 0 { banDatetime = time.Now().Unix() - newPrivileges = privileges &^ (common.UserPrivilegeNormal | common.UserPrivilegePublic) + privsSet = "privileges = (privileges & ~3)" } else { banDatetime = 0 - newPrivileges = privileges | (common.UserPrivilegeNormal | common.UserPrivilegePublic) + privsSet = "privileges = (privileges | 3)" } - _, err = md.DB.Exec("UPDATE users SET privileges = ?, ban_datetime = ? WHERE id = ?", newPrivileges, banDatetime, data.UserID) + _, err := md.DB.Exec("UPDATE users SET "+privsSet+", ban_datetime = ? WHERE id = ?", banDatetime, data.UserID) if err != nil { md.Err(err) return Err500 diff --git a/app/v1/ping.go b/app/v1/ping.go index 17e1ca9..f9a84cc 100644 --- a/app/v1/ping.go +++ b/app/v1/ping.go @@ -87,8 +87,8 @@ func surpriseMe() string { type pingResponse struct { common.ResponseBase - ID int `json:"user_id"` - Privileges int `json:"privileges"` + ID int `json:"user_id"` + Privileges uint64 `json:"privileges"` } // PingGET is a message to check with the API that we are logged in, and know what are our privileges. @@ -103,7 +103,7 @@ func PingGET(md common.MethodData) common.CodeMessager { } r.ID = md.ID() - r.Privileges = int(md.User.Privileges) + r.Privileges = uint64(md.User.TokenPrivileges) return r } diff --git a/app/v1/privileges.go b/app/v1/privileges.go deleted file mode 100644 index cc8cc87..0000000 --- a/app/v1/privileges.go +++ /dev/null @@ -1,43 +0,0 @@ -package v1 - -import ( - "git.zxq.co/ripple/rippleapi/common" -) - -type privilegesData struct { - common.ResponseBase - Read bool `json:"read"` - ReadConfidential bool `json:"read_confidential"` - Write bool `json:"write"` - ManageBadges bool `json:"manage_badges"` - BetaKeys bool `json:"beta_keys"` - ManageSettings bool `json:"manage_settings"` - ViewUserAdvanced bool `json:"view_user_advanced"` - ManageUser bool `json:"manage_user"` - ManageRoles bool `json:"manage_roles"` - ManageAPIKeys bool `json:"manage_api_keys"` - Blog bool `json:"blog"` - APIMeta bool `json:"api_meta"` - Beatmap bool `json:"beatmap"` -} - -// PrivilegesGET returns an explaination for the privileges, telling the client what they can do with this token. -func PrivilegesGET(md common.MethodData) common.CodeMessager { - r := privilegesData{} - r.Code = 200 - // This code sucks. - r.Read = true - r.ReadConfidential = md.User.Privileges.HasPrivilegeReadConfidential() - r.Write = md.User.Privileges.HasPrivilegeWrite() - r.ManageBadges = md.User.Privileges.HasPrivilegeManageBadges() - r.BetaKeys = md.User.Privileges.HasPrivilegeBetaKeys() - r.ManageSettings = md.User.Privileges.HasPrivilegeManageSettings() - r.ViewUserAdvanced = md.User.Privileges.HasPrivilegeViewUserAdvanced() - r.ManageUser = md.User.Privileges.HasPrivilegeManageUser() - r.ManageRoles = md.User.Privileges.HasPrivilegeManageRoles() - r.ManageAPIKeys = md.User.Privileges.HasPrivilegeManageAPIKeys() - r.Blog = md.User.Privileges.HasPrivilegeBlog() - r.APIMeta = md.User.Privileges.HasPrivilegeAPIMeta() - r.Beatmap = md.User.Privileges.HasPrivilegeBeatmap() - return r -} diff --git a/app/v1/score.go b/app/v1/score.go index dc37148..650b16f 100644 --- a/app/v1/score.go +++ b/app/v1/score.go @@ -83,7 +83,8 @@ SELECT FROM scores INNER JOIN users ON users.id = scores.userid INNER JOIN users_stats ON users_stats.id = scores.userid -WHERE scores.beatmap_md5 = ? AND scores.completed = '3' AND users.privileges & 1 > 0 `+genModeClause(md)+` +WHERE scores.beatmap_md5 = ? AND scores.completed = '3' AND `+md.User.OnlyUserPublic(true)+ + ` `+genModeClause(md)+` `+sort+common.Paginate(md.Query("p"), md.Query("l"), 100), beatmapMD5) if err != nil { md.Err(err) diff --git a/app/v1/token.go b/app/v1/token.go index 96399d8..b8f4439 100644 --- a/app/v1/token.go +++ b/app/v1/token.go @@ -60,13 +60,13 @@ func TokenNewPOST(md common.MethodData) common.CodeMessager { } var ( - rank int - pw string - pwVersion int - privileges int + rank int + pw string + pwVersion int + privilegesRaw uint64 ) - err = q.Scan(&r.ID, &r.Username, &rank, &pw, &pwVersion, &privileges) + err = q.Scan(&r.ID, &r.Username, &rank, &pw, &pwVersion, &privilegesRaw) switch { case err == sql.ErrNoRows: return common.SimpleResponse(404, "No user with that username/id was found.") @@ -74,6 +74,7 @@ func TokenNewPOST(md common.MethodData) common.CodeMessager { md.Err(err) return Err500 } + privileges := common.UserPrivileges(privilegesRaw) if nFailedAttempts(r.ID) > 20 { return common.SimpleResponse(429, "You've made too many login attempts. Try again later.") @@ -227,13 +228,18 @@ LEFT JOIN users ON users.id = tokens.user } for rows.Next() { var ( - id int - privsRaw uint64 - privs common.Privileges - newPrivs common.Privileges - privileges int + id int + privsRaw uint64 + privs common.Privileges + newPrivs common.Privileges + privilegesRaw uint64 ) - rows.Scan(&id, &privsRaw, &privileges) + err := rows.Scan(&id, &privsRaw, &privilegesRaw) + if err != nil { + fmt.Println(err) + continue + } + privileges := common.UserPrivileges(privilegesRaw) privs = common.Privileges(privsRaw) newPrivs = privs.CanOnly(privileges) if newPrivs != privs { diff --git a/app/v1/user.go b/app/v1/user.go index cfc3133..cdc0592 100644 --- a/app/v1/user.go +++ b/app/v1/user.go @@ -36,7 +36,7 @@ SELECT users.id, users.username, register_datetime, privileges, FROM users LEFT JOIN users_stats ON users.id=users_stats.id -WHERE ` + whereClause + ` AND users.privileges & 1 > 0 +WHERE ` + whereClause + ` AND ` + md.User.OnlyUserPublic(true) + ` LIMIT 1` return userPuts(md, md.DB.QueryRowx(query, param)) } @@ -92,10 +92,11 @@ type whatIDResponse struct { func UserWhatsTheIDGET(md common.MethodData) common.CodeMessager { var ( r whatIDResponse - privileges int + privileges uint64 ) err := md.DB.QueryRow("SELECT id, privileges FROM users WHERE username = ? LIMIT 1", md.Query("name")).Scan(&r.ID, &privileges) - if err != nil || ((privileges&common.UserPrivilegePublic) == 0 && !md.User.Privileges.HasPrivilegeViewUserAdvanced()) { + if err != nil || ((privileges&uint64(common.UserPrivilegePublic)) == 0 && + (md.User.UserPrivileges&common.AdminPrivilegeManageUsers == 0)) { return common.SimpleResponse(404, "That user could not be found!") } r.Code = 200 @@ -167,7 +168,7 @@ LEFT JOIN leaderboard_ctb ON users.id=leaderboard_ctb.user LEFT JOIN leaderboard_mania ON users.id=leaderboard_mania.user -WHERE ` + whereClause + ` AND users.privileges & 1 > 0 +WHERE ` + whereClause + ` AND ` + md.User.OnlyUserPublic(true) + ` LIMIT 1 ` // Fuck. @@ -278,7 +279,8 @@ func UserLookupGET(md common.MethodData) common.CodeMessager { return common.SimpleResponse(400, "please provide an username to start searching") } name = "%" + name + "%" - rows, err := md.DB.Query("SELECT users.id, users.username FROM users WHERE username LIKE ? AND privileges & 1 > 0 LIMIT 25", name) + rows, err := md.DB.Query("SELECT users.id, users.username FROM users WHERE username LIKE ? AND "+ + md.User.OnlyUserPublic(true)+" LIMIT 25", name) if err != nil { md.Err(err) return Err500 diff --git a/app/v1/user_scores.go b/app/v1/user_scores.go index 1236d9b..316b26c 100644 --- a/app/v1/user_scores.go +++ b/app/v1/user_scores.go @@ -52,7 +52,7 @@ func UserScoresBestGET(md common.MethodData) common.CodeMessager { scores.completed = '3' AND %s %s - AND users.privileges & 1 > 0 + AND `+md.User.OnlyUserPublic(true)+` ORDER BY scores.pp DESC, scores.score DESC %s`, wc, mc, common.Paginate(md.Query("p"), md.Query("l"), 100), ), param) @@ -68,7 +68,7 @@ func UserScoresRecentGET(md common.MethodData) common.CodeMessager { `WHERE %s %s - AND users.privileges & 1 > 0 + AND `+md.User.OnlyUserPublic(true)+` ORDER BY scores.time DESC %s`, wc, genModeClause(md), common.Paginate(md.Query("p"), md.Query("l"), 100), ), param) diff --git a/common/method_data.go b/common/method_data.go index 50c4877..15ecb78 100644 --- a/common/method_data.go +++ b/common/method_data.go @@ -30,6 +30,13 @@ func (md MethodData) Query(q string) string { return md.C.Query(q) } +// HasQuery returns true if the parameter is encountered in the querystring. +// It returns true even if the parameter is "" (the case of ?param&etc=etc) +func (md MethodData) HasQuery(q string) bool { + _, has := md.C.GetQuery(q) + return has +} + // RequestData is the body of a request. It is wrapped into this type // to implement the Unmarshal function, which is just a shorthand to // json.Unmarshal. diff --git a/common/privileges.go b/common/privileges.go index 8e1040b..e5bc3f0 100644 --- a/common/privileges.go +++ b/common/privileges.go @@ -22,66 +22,6 @@ const ( // Privileges is a bitwise enum of the privileges of an user's API key. type Privileges uint64 -// HasPrivilegeReadConfidential returns whether the ReadConfidential privilege is included in the privileges. -func (p Privileges) HasPrivilegeReadConfidential() bool { - return p&PrivilegeReadConfidential != 0 -} - -// HasPrivilegeWrite returns whether the Write privilege is included in the privileges. -func (p Privileges) HasPrivilegeWrite() bool { - return p&PrivilegeWrite != 0 -} - -// HasPrivilegeManageBadges returns whether the ManageBadges privilege is included in the privileges. -func (p Privileges) HasPrivilegeManageBadges() bool { - return p&PrivilegeManageBadges != 0 -} - -// HasPrivilegeBetaKeys returns whether the BetaKeys privilege is included in the privileges. -func (p Privileges) HasPrivilegeBetaKeys() bool { - return p&PrivilegeBetaKeys != 0 -} - -// HasPrivilegeManageSettings returns whether the ManageSettings privilege is included in the privileges. -func (p Privileges) HasPrivilegeManageSettings() bool { - return p&PrivilegeManageSettings != 0 -} - -// HasPrivilegeViewUserAdvanced returns whether the ViewUserAdvanced privilege is included in the privileges. -func (p Privileges) HasPrivilegeViewUserAdvanced() bool { - return p&PrivilegeViewUserAdvanced != 0 -} - -// HasPrivilegeManageUser returns whether the ManageUser privilege is included in the privileges. -func (p Privileges) HasPrivilegeManageUser() bool { - return p&PrivilegeManageUser != 0 -} - -// HasPrivilegeManageRoles returns whether the ManageRoles privilege is included in the privileges. -func (p Privileges) HasPrivilegeManageRoles() bool { - return p&PrivilegeManageRoles != 0 -} - -// HasPrivilegeManageAPIKeys returns whether the ManageAPIKeys privilege is included in the privileges. -func (p Privileges) HasPrivilegeManageAPIKeys() bool { - return p&PrivilegeManageAPIKeys != 0 -} - -// HasPrivilegeBlog returns whether the Blog privilege is included in the privileges. -func (p Privileges) HasPrivilegeBlog() bool { - return p&PrivilegeBlog != 0 -} - -// HasPrivilegeAPIMeta returns whether the APIMeta privilege is included in the privileges. -func (p Privileges) HasPrivilegeAPIMeta() bool { - return p&PrivilegeAPIMeta != 0 -} - -// HasPrivilegeBeatmap returns whether the Beatmap privilege is included in the privileges. -func (p Privileges) HasPrivilegeBeatmap() bool { - return p&PrivilegeBeatmap != 0 -} - var privilegeString = [...]string{ "Read", "ReadConfidential", @@ -101,14 +41,14 @@ var privilegeString = [...]string{ func (p Privileges) String() string { var pvs []string for i, v := range privilegeString { - if int(p)&(1<)" +// if the user does not have the UserPrivilege AdminManageUsers, and returns "1" otherwise. +func (t Token) OnlyUserPublic(userManagerSeesEverything bool) string { + if userManagerSeesEverything && + t.UserPrivileges&AdminPrivilegeManageUsers == AdminPrivilegeManageUsers { + return "1" + } + // It's safe to use sprintf directly even if it's a query, because UserID is an int. + return fmt.Sprintf("(user.privileges & 1 = 1 OR users.id = '%d')", t.UserID) } diff --git a/common/user_privileges.go b/common/user_privileges.go index 046b614..b1a7dfe 100644 --- a/common/user_privileges.go +++ b/common/user_privileges.go @@ -1,8 +1,10 @@ package common +import "strings" + // user/admin privileges const ( - UserPrivilegePublic = 1 << iota + UserPrivilegePublic UserPrivileges = 1 << iota UserPrivilegeNormal UserPrivilegeDonor AdminPrivilegeAccessRAP @@ -22,4 +24,42 @@ const ( AdminPrivilegeSendAlerts AdminPrivilegeChatMod AdminPrivilegeKickUsers + UserPrivilegePendingVerification ) + +// UserPrivileges represents a bitwise enum of the privileges of an user. +type UserPrivileges uint64 + +var userPrivilegeString = [...]string{ + "UserPublic", + "UserNormal", + "UserDonor", + "AdminAccessRAP", + "AdminManageUsers", + "AdminBanUsers", + "AdminSilenceUsers", + "AdminWipeUsers", + "AdminManageBeatmap", + "AdminManageServer", + "AdminManageSetting", + "AdminManageBetaKey", + "AdminManageReport", + "AdminManageDocs", + "AdminManageBadges", + "AdminViewRAPLogs", + "AdminManagePrivilege", + "AdminSendAlerts", + "AdminChatMod", + "AdminKickUsers", + "UserPendingVerification", +} + +func (p UserPrivileges) String() string { + var pvs []string + for i, v := range userPrivilegeString { + if uint64(p)&uint64(1<