Require client to specify explicitly in websockets whether restricted users should be seen

This is only allowed to those having the user privilege AdminPrivilegeManageUsers, having being identified by the API AND having sent a message of type set_restricted_visibility stating specifically in the data that they want to get info also about restricted users.
This also includes some more information in the new_scores, such as the username and userid of the user who submitted the score.
This commit is contained in:
Morgan Bazalgette 2017-07-25 14:49:14 +02:00
parent 60d48df46d
commit 8ebe5f6a02
No known key found for this signature in database
GPG Key ID: 40D328300D245DA5
7 changed files with 225 additions and 38 deletions

View File

@ -5,7 +5,6 @@ import (
"crypto/sha256" "crypto/sha256"
"database/sql" "database/sql"
"fmt" "fmt"
"strings"
"time" "time"
"github.com/jmoiron/sqlx" "github.com/jmoiron/sqlx"
@ -87,7 +86,7 @@ func tokenUpdater(db *sqlx.DB) {
} }
} }
// BearerToken parses a Token guiven in the Authorization header, with the // BearerToken parses a Token given in the Authorization header, with the
// Bearer prefix. // Bearer prefix.
func BearerToken(token string, db *sqlx.DB) (common.Token, bool) { func BearerToken(token string, db *sqlx.DB) (common.Token, bool) {
var x struct { var x struct {
@ -107,20 +106,7 @@ func BearerToken(token string, db *sqlx.DB) (common.Token, bool) {
t.UserID = x.Extra t.UserID = x.Extra
t.Value = token t.Value = token
t.UserPrivileges = common.UserPrivileges(privs) t.UserPrivileges = common.UserPrivileges(privs)
t.TokenPrivileges = oauthPrivileges(x.Scope).CanOnly(t.UserPrivileges) t.TokenPrivileges = common.OAuthPrivileges(x.Scope).CanOnly(t.UserPrivileges)
return t, true return t, true
} }
var privilegeMap = map[string]common.Privileges{
"read_confidential": common.PrivilegeReadConfidential,
"write": common.PrivilegeWrite,
}
func oauthPrivileges(scopes string) common.Privileges {
var p common.Privileges
for _, x := range strings.Split(scopes, " ") {
p |= privilegeMap[x]
}
return p
}

101
app/websockets/identify.go Normal file
View File

@ -0,0 +1,101 @@
package websockets
import (
"crypto/md5"
"crypto/sha256"
"encoding/json"
"fmt"
"database/sql"
"zxq.co/ripple/rippleapi/common"
)
type websocketUser struct {
ID int `json:"id"`
Username string `json:"username"`
UserPrivileges uint64 `json:"user_privileges"`
TokenPrivileges uint64 `json:"token_privileges"`
ApplicationID *string `json:"application_id"`
}
type identifyMessage struct {
Token string `json:"token"`
IsBearer bool `json:"is_bearer"`
}
// Identify sets the identity of the user.
func Identify(c *conn, message incomingMessage) {
var idMsg identifyMessage
err := json.Unmarshal(message.Data, &idMsg)
if err != nil {
c.WriteJSON(TypeInvalidMessage, err.Error())
return
}
var wsu websocketUser
if idMsg.IsBearer {
err = getBearerToken(idMsg.Token, &wsu)
} else {
err = db.Get(&wsu, `
SELECT
t.user as id, t.privileges as token_privileges,
u.username, u.privileges as user_privileges
FROM tokens t
INNER JOIN users u ON t.user = u.id
WHERE t.token = ?`, fmt.Sprintf("%x", md5.Sum([]byte(idMsg.Token))))
}
switch err {
case nil:
break
case sql.ErrNoRows:
c.WriteJSON(TypeNotFound, nil)
return
default:
common.WSErr(err)
c.WriteJSON(TypeUnexpectedError, nil)
return
}
wsu.TokenPrivileges = uint64(
common.Privileges(wsu.TokenPrivileges).CanOnly(
common.UserPrivileges(wsu.UserPrivileges),
),
)
c.Mtx.Lock()
c.User = &wsu
c.Mtx.Unlock()
c.WriteJSON(TypeIdentified, wsu)
}
func getBearerToken(token string, wsu *websocketUser) error {
var x struct {
Client string
Scope string
Extra int
}
err := db.Get(&x, "SELECT client, scope, extra FROM osin_access WHERE access_token = ? LIMIT 1", fmt.Sprintf("%x", sha256.Sum256([]byte(token))))
if err != nil {
return err
}
var userInfo struct {
Username string
Privileges uint64
}
err = db.Get(&userInfo, "SELECT username, privileges FROM users WHERE id = ? LIMIT 1", x.Extra)
if err != nil {
return err
}
wsu.ApplicationID = &x.Client
wsu.ID = x.Extra
wsu.Username = userInfo.Username
wsu.UserPrivileges = userInfo.Privileges
wsu.TokenPrivileges = uint64(common.OAuthPrivileges(x.Scope))
return nil
}

View File

@ -26,6 +26,8 @@ func handler(rawConn *websocket.Conn) {
rawConn, rawConn,
sync.Mutex{}, sync.Mutex{},
step | uint64(time.Now().UnixNano()<<10), step | uint64(time.Now().UnixNano()<<10),
false,
nil,
} }
c.WriteJSON(TypeConnected, nil) c.WriteJSON(TypeConnected, nil)
@ -57,6 +59,8 @@ type conn struct {
Conn *websocket.Conn Conn *websocket.Conn
Mtx sync.Mutex Mtx sync.Mutex
ID uint64 ID uint64
RestrictedVisible bool
User *websocketUser
} }
func (c *conn) WriteJSON(t string, data interface{}) error { func (c *conn) WriteJSON(t string, data interface{}) error {
@ -68,6 +72,8 @@ func (c *conn) WriteJSON(t string, data interface{}) error {
var messageHandler = map[string]func(c *conn, message incomingMessage){ var messageHandler = map[string]func(c *conn, message incomingMessage){
TypeSubscribeScores: SubscribeScores, TypeSubscribeScores: SubscribeScores,
TypeSetRestrictedVisibility: SetRestrictedVisibility,
TypeIdentify: Identify,
TypePing: pingHandler, TypePing: pingHandler,
} }
@ -75,14 +81,20 @@ var messageHandler = map[string]func(c *conn, message incomingMessage){
const ( const (
TypeConnected = "connected" TypeConnected = "connected"
TypeInvalidMessage = "invalid_message_type" TypeInvalidMessage = "invalid_message_type"
TypeUnexpectedError = "unexpected_error"
TypeNotFound = "not_found"
TypeSubscribedToScores = "subscribed_to_scores" TypeSubscribedToScores = "subscribed_to_scores"
TypeNewScore = "new_score" TypeNewScore = "new_score"
TypeIdentified = "identified"
TypeRestrictedVisibilitySet = "restricted_visibility_set"
TypePong = "pong" TypePong = "pong"
) )
// Client Message Types // Client Message Types
const ( const (
TypeSubscribeScores = "subscribe_scores" TypeSubscribeScores = "subscribe_scores"
TypeIdentify = "identify"
TypeSetRestrictedVisibility = "set_restricted_visibility"
TypePing = "ping" TypePing = "ping"
) )

View File

@ -0,0 +1,31 @@
package websockets
import (
"encoding/json"
"zxq.co/ripple/rippleapi/common"
)
// SetRestrictedVisibility sets whether the information of restricted users
// can be seen.
func SetRestrictedVisibility(c *conn, message incomingMessage) {
var visibility bool
err := json.Unmarshal(message.Data, &visibility)
if err != nil {
c.WriteJSON(TypeInvalidMessage, err.Error())
return
}
var userIsManager bool
if c.User != nil && (c.User.UserPrivileges&uint64(common.AdminPrivilegeManageUsers) > 0) {
userIsManager = true
}
c.Mtx.Lock()
visibility = visibility && userIsManager
c.RestrictedVisible = visibility
c.Mtx.Unlock()
c.WriteJSON(TypeRestrictedVisibilitySet, visibility)
}

View File

@ -8,6 +8,7 @@ import (
"gopkg.in/thehowl/go-osuapi.v1" "gopkg.in/thehowl/go-osuapi.v1"
"zxq.co/ripple/rippleapi/app/v1" "zxq.co/ripple/rippleapi/app/v1"
"zxq.co/ripple/rippleapi/common"
"zxq.co/x/getrank" "zxq.co/x/getrank"
) )
@ -69,9 +70,21 @@ func scoreRetriever() {
} }
} }
type scoreUser struct {
UserID int `json:"id"`
Username string `json:"username"`
Privileges uint64 `json:"privileges"`
}
type score struct { type score struct {
v1.Score
scoreUser
}
type scoreJSON struct {
v1.Score v1.Score
UserID int `json:"user_id"` UserID int `json:"user_id"`
User scoreUser `json:"user"`
} }
func handleNewScore(id string) { func handleNewScore(id string) {
@ -79,10 +92,13 @@ func handleNewScore(id string) {
var s score var s score
err := db.Get(&s, ` err := db.Get(&s, `
SELECT SELECT
id, beatmap_md5, score, max_combo, full_combo, mods, s.id, s.beatmap_md5, s.score, s.max_combo, s.full_combo, s.mods,
300_count, 100_count, 50_count, gekis_count, katus_count, misses_count, s.300_count, s.100_count, s.50_count, s.gekis_count, s.katus_count, s.misses_count,
time, play_mode, accuracy, pp, completed, userid AS user_id s.time, s.play_mode, s.accuracy, s.pp, s.completed, s.userid AS user_id,
FROM scores WHERE id = ?`, id) u.username, u.privileges
FROM scores s
INNER JOIN users u ON s.userid = u.id
WHERE s.id = ?`, id)
if err != nil { if err != nil {
fmt.Println(err) fmt.Println(err)
return return
@ -96,21 +112,32 @@ FROM scores WHERE id = ?`, id)
s.Count50, s.Count50,
s.CountMiss, s.CountMiss,
)) ))
sj := scoreJSON{
Score: s.Score,
UserID: s.UserID,
User: s.scoreUser,
}
scoreSubscriptionsMtx.RLock() scoreSubscriptionsMtx.RLock()
cp := make([]scoreSubscription, len(scoreSubscriptions)) cp := make([]scoreSubscription, len(scoreSubscriptions))
copy(cp, scoreSubscriptions) copy(cp, scoreSubscriptions)
scoreSubscriptionsMtx.RUnlock() scoreSubscriptionsMtx.RUnlock()
for _, el := range cp { for _, el := range cp {
if len(el.Users) > 0 && !scoreUserValid(el.Users, s) { if len(el.Users) > 0 && !scoreUserValid(el.Users, sj) {
continue continue
} }
el.Conn.WriteJSON(TypeNewScore, s) if sj.User.Privileges&3 != 3 && !el.Conn.RestrictedVisible {
continue
}
el.Conn.WriteJSON(TypeNewScore, sj)
} }
} }
func scoreUserValid(users []subscribeScoresUser, s score) bool { func scoreUserValid(users []subscribeScoresUser, s scoreJSON) bool {
for _, u := range users { for _, u := range users {
if u.User == s.UserID { if u.User == s.UserID {
if len(u.Modes) > 0 { if len(u.Modes) > 0 {
@ -136,7 +163,11 @@ func inModes(modes []int, i int) bool {
func catchPanic() { func catchPanic() {
r := recover() r := recover()
if r != nil { if r != nil {
fmt.Println(r) switch r := r.(type) {
// TODO: sentry case error:
common.WSErr(r)
default:
fmt.Println("PANIC", r)
}
} }
} }

View File

@ -70,6 +70,13 @@ func Err(c *fasthttp.RequestCtx, err error) {
_err(err, tags, nil, c) _err(err, tags, nil, c)
} }
// WSErr is the error function for errors happening in the websockets.
func WSErr(err error) {
_err(err, map[string]string{
"endpoint": "/api/v1/ws",
}, nil, nil)
}
func _err(err error, tags map[string]string, user *raven.User, c *fasthttp.RequestCtx) { func _err(err error, tags map[string]string, user *raven.User, c *fasthttp.RequestCtx) {
if RavenClient == nil { if RavenClient == nil {
fmt.Println("ERROR!!!!") fmt.Println("ERROR!!!!")
@ -93,6 +100,10 @@ func _err(err error, tags map[string]string, user *raven.User, c *fasthttp.Reque
} }
func generateRavenHTTP(ctx *fasthttp.RequestCtx) *raven.Http { func generateRavenHTTP(ctx *fasthttp.RequestCtx) *raven.Http {
if ctx == nil {
return nil
}
// build uri // build uri
uri := ctx.URI() uri := ctx.URI()
// safe to use b2s because a new string gets allocated eventually for // safe to use b2s because a new string gets allocated eventually for

View File

@ -77,3 +77,18 @@ func (p Privileges) CanOnly(userPrivs UserPrivileges) Privileges {
} }
return Privileges(newPrivilege) return Privileges(newPrivilege)
} }
var privilegeMap = map[string]Privileges{
"read_confidential": PrivilegeReadConfidential,
"write": PrivilegeWrite,
}
// OAuthPrivileges returns the equivalent in Privileges of a space-separated
// list of scopes.
func OAuthPrivileges(scopes string) Privileges {
var p Privileges
for _, x := range strings.Split(scopes, " ") {
p |= privilegeMap[x]
}
return p
}