Allow users with AdminManageUsers to see banned users

Also:
- General code refactoring
- Allow banned/restricted users to see their scores etc
- common.MethodData now contains UserPrivileges
- UserPrivileges have now their own type
- Implement md.HasQuery, to know if there's a GET querystring parameter or not
This commit is contained in:
Howl 2016-08-27 12:04:12 +02:00
parent 476cd385f8
commit e4d27f8d6b
18 changed files with 130 additions and 159 deletions

View File

@ -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")

View File

@ -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))

View File

@ -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

View File

@ -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 (

View File

@ -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

View File

@ -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)
}

View File

@ -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)

View File

@ -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

View File

@ -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
}

View File

@ -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
}

View File

@ -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)

View File

@ -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 {

View File

@ -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

View File

@ -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)

View File

@ -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.

View File

@ -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<<uint(i)) != 0 {
if uint64(p)&uint64(1<<uint(i)) != 0 {
pvs = append(pvs, v)
}
}
return strings.Join(pvs, ", ")
}
var privilegeMustBe = [...]int{
var privilegeMustBe = [...]UserPrivileges{
1 << 30, // read is deprecated, and should be given out to no-one.
UserPrivilegeNormal,
UserPrivilegeNormal,
@ -125,7 +65,7 @@ var privilegeMustBe = [...]int{
}
// CanOnly removes any privilege that the user has requested to have, but cannot have due to their rank.
func (p Privileges) CanOnly(userPrivs int) Privileges {
func (p Privileges) CanOnly(userPrivs UserPrivileges) Privileges {
newPrivilege := 0
for i, v := range privilegeMustBe {
wants := p&1 == 1

View File

@ -1,9 +1,23 @@
package common
// Token Is an API token.
import "fmt"
// Token is an API token.
type Token struct {
ID int
Value string
UserID int
Privileges Privileges
ID int
Value string
UserID int
TokenPrivileges Privileges
UserPrivileges UserPrivileges
}
// OnlyUserPublic returns a string containing "(user.privileges & 1 = 1 OR users.id = <userID>)"
// 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)
}

View File

@ -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<<uint(i)) != 0 {
pvs = append(pvs, v)
}
}
return strings.Join(pvs, ", ")
}