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:
parent
60d48df46d
commit
8ebe5f6a02
|
@ -5,7 +5,6 @@ import (
|
|||
"crypto/sha256"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"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.
|
||||
func BearerToken(token string, db *sqlx.DB) (common.Token, bool) {
|
||||
var x struct {
|
||||
|
@ -107,20 +106,7 @@ func BearerToken(token string, db *sqlx.DB) (common.Token, bool) {
|
|||
t.UserID = x.Extra
|
||||
t.Value = token
|
||||
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
|
||||
}
|
||||
|
||||
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
101
app/websockets/identify.go
Normal 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
|
||||
}
|
|
@ -26,6 +26,8 @@ func handler(rawConn *websocket.Conn) {
|
|||
rawConn,
|
||||
sync.Mutex{},
|
||||
step | uint64(time.Now().UnixNano()<<10),
|
||||
false,
|
||||
nil,
|
||||
}
|
||||
|
||||
c.WriteJSON(TypeConnected, nil)
|
||||
|
@ -57,6 +59,8 @@ type conn struct {
|
|||
Conn *websocket.Conn
|
||||
Mtx sync.Mutex
|
||||
ID uint64
|
||||
RestrictedVisible bool
|
||||
User *websocketUser
|
||||
}
|
||||
|
||||
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){
|
||||
TypeSubscribeScores: SubscribeScores,
|
||||
TypeSetRestrictedVisibility: SetRestrictedVisibility,
|
||||
TypeIdentify: Identify,
|
||||
TypePing: pingHandler,
|
||||
}
|
||||
|
||||
|
@ -75,14 +81,20 @@ var messageHandler = map[string]func(c *conn, message incomingMessage){
|
|||
const (
|
||||
TypeConnected = "connected"
|
||||
TypeInvalidMessage = "invalid_message_type"
|
||||
TypeUnexpectedError = "unexpected_error"
|
||||
TypeNotFound = "not_found"
|
||||
TypeSubscribedToScores = "subscribed_to_scores"
|
||||
TypeNewScore = "new_score"
|
||||
TypeIdentified = "identified"
|
||||
TypeRestrictedVisibilitySet = "restricted_visibility_set"
|
||||
TypePong = "pong"
|
||||
)
|
||||
|
||||
// Client Message Types
|
||||
const (
|
||||
TypeSubscribeScores = "subscribe_scores"
|
||||
TypeIdentify = "identify"
|
||||
TypeSetRestrictedVisibility = "set_restricted_visibility"
|
||||
TypePing = "ping"
|
||||
)
|
||||
|
||||
|
|
31
app/websockets/restricted_visibility.go
Normal file
31
app/websockets/restricted_visibility.go
Normal 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)
|
||||
}
|
|
@ -8,6 +8,7 @@ import (
|
|||
|
||||
"gopkg.in/thehowl/go-osuapi.v1"
|
||||
"zxq.co/ripple/rippleapi/app/v1"
|
||||
"zxq.co/ripple/rippleapi/common"
|
||||
"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 {
|
||||
v1.Score
|
||||
scoreUser
|
||||
}
|
||||
|
||||
type scoreJSON struct {
|
||||
v1.Score
|
||||
UserID int `json:"user_id"`
|
||||
User scoreUser `json:"user"`
|
||||
}
|
||||
|
||||
func handleNewScore(id string) {
|
||||
|
@ -79,10 +92,13 @@ func handleNewScore(id string) {
|
|||
var s score
|
||||
err := db.Get(&s, `
|
||||
SELECT
|
||||
id, beatmap_md5, score, max_combo, full_combo, mods,
|
||||
300_count, 100_count, 50_count, gekis_count, katus_count, misses_count,
|
||||
time, play_mode, accuracy, pp, completed, userid AS user_id
|
||||
FROM scores WHERE id = ?`, id)
|
||||
s.id, s.beatmap_md5, s.score, s.max_combo, s.full_combo, s.mods,
|
||||
s.300_count, s.100_count, s.50_count, s.gekis_count, s.katus_count, s.misses_count,
|
||||
s.time, s.play_mode, s.accuracy, s.pp, s.completed, s.userid AS user_id,
|
||||
u.username, u.privileges
|
||||
FROM scores s
|
||||
INNER JOIN users u ON s.userid = u.id
|
||||
WHERE s.id = ?`, id)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return
|
||||
|
@ -96,21 +112,32 @@ FROM scores WHERE id = ?`, id)
|
|||
s.Count50,
|
||||
s.CountMiss,
|
||||
))
|
||||
|
||||
sj := scoreJSON{
|
||||
Score: s.Score,
|
||||
UserID: s.UserID,
|
||||
User: s.scoreUser,
|
||||
}
|
||||
|
||||
scoreSubscriptionsMtx.RLock()
|
||||
cp := make([]scoreSubscription, len(scoreSubscriptions))
|
||||
copy(cp, scoreSubscriptions)
|
||||
scoreSubscriptionsMtx.RUnlock()
|
||||
|
||||
for _, el := range cp {
|
||||
if len(el.Users) > 0 && !scoreUserValid(el.Users, s) {
|
||||
if len(el.Users) > 0 && !scoreUserValid(el.Users, sj) {
|
||||
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 {
|
||||
if u.User == s.UserID {
|
||||
if len(u.Modes) > 0 {
|
||||
|
@ -136,7 +163,11 @@ func inModes(modes []int, i int) bool {
|
|||
func catchPanic() {
|
||||
r := recover()
|
||||
if r != nil {
|
||||
fmt.Println(r)
|
||||
// TODO: sentry
|
||||
switch r := r.(type) {
|
||||
case error:
|
||||
common.WSErr(r)
|
||||
default:
|
||||
fmt.Println("PANIC", r)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -70,6 +70,13 @@ func Err(c *fasthttp.RequestCtx, err error) {
|
|||
_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) {
|
||||
if RavenClient == nil {
|
||||
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 {
|
||||
if ctx == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// build uri
|
||||
uri := ctx.URI()
|
||||
// safe to use b2s because a new string gets allocated eventually for
|
||||
|
|
|
@ -77,3 +77,18 @@ func (p Privileges) CanOnly(userPrivs UserPrivileges) Privileges {
|
|||
}
|
||||
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
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue
Block a user