Skip to content

Commit 2a6c059

Browse files
committed
major refactor topic api v1, before adding v2
1 parent 441c82e commit 2a6c059

File tree

2 files changed

+105
-104
lines changed

2 files changed

+105
-104
lines changed

users/mqtt.py

+81-77
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,13 @@
1010
SCENE_PUBLIC_WRITE_DEF, SCENE_USERS_DEF,
1111
SCENE_VIDEO_CONF_DEF, Scene)
1212

13-
# topic constants
1413
PUBLIC_NAMESPACE = "public"
1514
ANON_REGEX = "anonymous-(?=.*?[a-zA-Z].*?[a-zA-Z])"
1615
DEF_JWT_DURATION = datetime.timedelta(minutes=1)
17-
API_V1 = "v1"
18-
API_V2 = "v2"
16+
17+
# version constants
18+
API_V1 = "v1" # url /user/, first version
19+
API_V2 = "v2" # url /user/v2/, full topic structure refactor
1920
TOPIC_SUPPORTED_API_VERSIONS = [API_V1, API_V2] # TODO (mwfarb): remove v1
2021

2122

@@ -30,11 +31,10 @@ def all_scenes_read_token(version):
3031

3132
realm = config["mqtt_realm"]
3233
username = config["mqtt_username"]
33-
duration = datetime.timedelta(minutes=1)
3434

3535
payload = {}
3636
payload["sub"] = username
37-
payload["exp"] = datetime.datetime.utcnow() + duration
37+
payload["exp"] = datetime.datetime.utcnow() + DEF_JWT_DURATION
3838

3939
if version == API_V2:
4040
payload["subs"] = [f"{realm}/s/+/+/o/#"] # v2
@@ -49,37 +49,20 @@ def generate_arena_token(
4949
*,
5050
user,
5151
username,
52-
realm="realm",
52+
realm=None,
5353
ns_scene=None,
54-
device=None,
55-
camid=None,
56-
userid=None,
57-
handleftid=None,
58-
handrightid=None,
54+
ns_device=None,
55+
ids=None,
5956
duration=DEF_JWT_DURATION
6057
):
61-
""" MQTT Token Constructor. Topic Notes:
62-
/s/: virtual scene objects
63-
/d/: device inter-process
64-
/env/: physical environment detection
65-
66-
Args:
67-
user (object): User object
68-
username (str): _description_
69-
realm (str, optional): _description_. Defaults to "realm".
70-
ns_scene (str, optional): _description_. Defaults to None.
71-
device (str, optional): _description_. Defaults to None.
72-
camid (str, optional): _description_. Defaults to None.
73-
userid (str, optional): _description_. Defaults to None.
74-
handleftid (str, optional): _description_. Defaults to None.
75-
handrightid (str, optional): _description_. Defaults to None.
76-
duration (integer, optional): _description_. Defaults to DEF_JWT_DURATION.
58+
""" MQTT Token Constructor.
7759
7860
Returns:
7961
str: JWT or None
8062
"""
81-
subs = []
82-
pubs = []
63+
config = settings.PUBSUB
64+
if not realm:
65+
realm = config["mqtt_realm"]
8366
privkeyfile = settings.MQTT_TOKEN_PRIVKEY
8467
if not os.path.exists(privkeyfile):
8568
print("Error: keyfile not found")
@@ -91,43 +74,75 @@ def generate_arena_token(
9174
payload["exp"] = datetime.datetime.utcnow() + duration
9275
headers = None
9376

94-
p_public_read = SCENE_PUBLIC_READ_DEF
95-
p_public_write = SCENE_PUBLIC_WRITE_DEF
96-
p_anonymous_users = SCENE_ANON_USERS_DEF
97-
p_video = SCENE_VIDEO_CONF_DEF
98-
p_users = SCENE_USERS_DEF
99-
100-
# create permissions shorthand
77+
perm = {
78+
"public_read": SCENE_PUBLIC_READ_DEF,
79+
"public_write": SCENE_PUBLIC_WRITE_DEF,
80+
"anonymous_users": SCENE_ANON_USERS_DEF,
81+
"video": SCENE_VIDEO_CONF_DEF,
82+
"users": SCENE_USERS_DEF,
83+
}
10184
if ns_scene and Scene.objects.filter(name=ns_scene).exists():
102-
scene_perm = Scene.objects.get(name=ns_scene)
103-
p_public_read = scene_perm.public_read
104-
p_public_write = scene_perm.public_write
105-
p_anonymous_users = scene_perm.anonymous_users
106-
p_video = scene_perm.video_conference
107-
p_users = scene_perm.users
85+
p = Scene.objects.get(name=ns_scene)
86+
perm["public_read"] = p.public_read
87+
perm["public_write"] = p.public_write
88+
perm["anonymous_users"] = p.anonymous_users
89+
perm["video"] = p.video_conference
90+
perm["users"] = p.users
10891

10992
# add jitsi server params if a/v scene
110-
if ns_scene and camid and p_users and p_video:
93+
if ns_scene and ids and perm["users"] and perm["video"]:
11194
host = os.getenv("HOSTNAME")
11295
headers = {"kid": host}
11396
payload["aud"] = "arena"
11497
payload["iss"] = "arena-account"
115-
# we use the namespace + scene name as the jitsi room name, handle RFC 3986 reserved chars as = '_'
98+
# we use the scene name as the jitsi room name, handle RFC 3986 reserved chars as = '_'
11699
roomname = re.sub(r"[!#$&'()*+,\/:;=?@[\]]", '_', ns_scene.lower())
117100
payload["room"] = roomname
118101

102+
pubs, subs = get_pubsub_topics_api_v1(
103+
user,
104+
username,
105+
realm,
106+
ns_scene,
107+
ns_device,
108+
ids,
109+
perm,
110+
)
111+
if len(subs) > 0:
112+
payload["subs"] = clean_topics(subs)
113+
if len(pubs) > 0:
114+
payload["publ"] = clean_topics(pubs)
115+
116+
return jwt.encode(payload, private_key, algorithm="RS256", headers=headers)
117+
118+
119+
def get_pubsub_topics_api_v1(
120+
user,
121+
username,
122+
realm,
123+
ns_scene,
124+
ns_device,
125+
ids,
126+
perm,
127+
):
128+
""" V1 Topic Notes:
129+
/s/: virtual scene objects
130+
/d/: device inter-process
131+
/env/: physical environment detection
132+
"""
133+
pubs = []
134+
subs = []
119135
# everyone should be able to read all public scenes
120-
if not device: # scene token scenario
136+
if not ns_device: # scene token scenario
121137
subs.append(f"{realm}/s/{PUBLIC_NAMESPACE}/#")
122138
# And transmit env data
123139
pubs.append(f"{realm}/env/{PUBLIC_NAMESPACE}/#")
124-
125140
# user presence objects
126141
if user.is_authenticated:
127-
if device: # device token scenario
142+
if ns_device: # device token scenario
128143
# device owners have rights to their device objects only
129-
subs.append(f"{realm}/d/{device}/#")
130-
pubs.append(f"{realm}/d/{device}/#")
144+
subs.append(f"{realm}/d/{ns_device}/#")
145+
pubs.append(f"{realm}/d/{ns_device}/#")
131146
else: # scene token scenario
132147
# scene rights default by namespace
133148
if user.is_staff:
@@ -164,58 +179,47 @@ def generate_arena_token(
164179
# device owners have rights to their device objects only
165180
subs.append(f"{realm}/d/{username}/#")
166181
pubs.append(f"{realm}/d/{username}/#")
167-
168182
# anon/non-owners have rights to view scene objects only
169183
if ns_scene and not user.is_staff:
170184
# did the user set specific public read or public write?
171-
if not user.is_authenticated and not p_anonymous_users:
185+
if not user.is_authenticated and not perm["anonymous_users"]:
172186
return None # anonymous not permitted
173-
if p_public_read:
187+
if perm["public_read"]:
174188
subs.append(f"{realm}/s/{ns_scene}/#")
175189
# Interactivity to extent of viewing objects is similar to publishing env
176190
pubs.append(f"{realm}/env/{ns_scene}/#")
177-
if p_public_write:
191+
if perm["public_write"]:
178192
pubs.append(f"{realm}/s/{ns_scene}/#")
179193
# user presence objects
180-
if camid and p_users: # probable web browser write
181-
pubs.append(f"{realm}/s/{ns_scene}/{camid}")
182-
pubs.append(f"{realm}/s/{ns_scene}/{camid}/#")
183-
if handleftid and p_users:
184-
pubs.append(f"{realm}/s/{ns_scene}/{handleftid}")
185-
if handrightid and p_users:
186-
pubs.append(f"{realm}/s/{ns_scene}/{handrightid}")
187-
194+
if ids and perm["users"]: # probable web browser write
195+
pubs.append(f"{realm}/s/{ns_scene}/{ids['camid']}")
196+
pubs.append(f"{realm}/s/{ns_scene}/{ids['camid']}/#")
197+
pubs.append(f"{realm}/s/{ns_scene}/{ids['handleftid']}")
198+
pubs.append(f"{realm}/s/{ns_scene}/{ids['handrightid']}")
188199
# chat messages
189-
if ns_scene and userid and p_users:
200+
if ns_scene and ids and perm["users"]:
190201
namespace = ns_scene.split("/")[0]
202+
userhandle = ids["userid"] + \
203+
base64.b64encode(ids["userid"].encode()).decode()
191204
# receive private messages: Read
192-
subs.append(f"{realm}/c/{namespace}/p/{userid}/#")
205+
subs.append(f"{realm}/c/{namespace}/p/{ids['userid']}/#")
193206
# receive open messages to everyone and/or scene: Read
194207
subs.append(f"{realm}/c/{namespace}/o/#")
195208
# send open messages (chat keepalive, messages to all/scene): Write
196-
pubs.append(f"{realm}/c/{namespace}/o/{userid}")
209+
pubs.append(f"{realm}/c/{namespace}/o/{userhandle}")
197210
# private messages to user: Write
198-
pubs.append(f"{realm}/c/{namespace}/p/+/{userid}")
199-
211+
pubs.append(f"{realm}/c/{namespace}/p/+/{userhandle}")
200212
# apriltags
201213
if ns_scene:
202214
subs.append(f"{realm}/g/a/#")
203215
pubs.append(f"{realm}/g/a/#")
204-
205-
# runtime manager
216+
# arts runtime-mngr
206217
subs.append(f"{realm}/proc/#")
207218
pubs.append(f"{realm}/proc/#")
208-
209-
# network metrics
219+
# network graph
210220
subs.append("$NETWORK")
211221
pubs.append("$NETWORK/latency")
212-
213-
if len(subs) > 0:
214-
payload["subs"] = clean_topics(subs)
215-
if len(pubs) > 0:
216-
payload["publ"] = clean_topics(pubs)
217-
218-
return jwt.encode(payload, private_key, algorithm="RS256", headers=headers)
222+
return pubs, subs
219223

220224

221225
def clean_topics(topics):

users/views.py

+24-27
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,9 @@
2626
from .forms import (DeviceForm, SceneForm, SocialSignupForm, UpdateDeviceForm,
2727
UpdateSceneForm, UpdateStaffForm)
2828
from .models import Device, Scene
29-
from .mqtt import (ANON_REGEX, PUBLIC_NAMESPACE, TOPIC_SUPPORTED_API_VERSIONS,
30-
all_scenes_read_token, generate_arena_token)
29+
from .mqtt import (ANON_REGEX, API_V2, PUBLIC_NAMESPACE,
30+
TOPIC_SUPPORTED_API_VERSIONS, all_scenes_read_token,
31+
generate_arena_token)
3132
from .persistence import (delete_scene_objects, get_persist_scenes_all,
3233
get_persist_scenes_ns)
3334
from .serializers import SceneNameSerializer, SceneSerializer
@@ -211,7 +212,7 @@ def device_perm_detail(request, pk):
211212
token = generate_arena_token(
212213
user=request.user,
213214
username=request.user.username,
214-
device=device.name,
215+
ns_device=device.name,
215216
duration=datetime.timedelta(days=30)
216217
)
217218

@@ -371,7 +372,8 @@ def my_scenes(request):
371372
except (ValueError, SocialAccount.DoesNotExist) as err:
372373
return JsonResponse({"error": err}, status=status.HTTP_403_FORBIDDEN)
373374

374-
serializer = SceneNameSerializer(get_my_scenes(user, request.version), many=True)
375+
serializer = SceneNameSerializer(
376+
get_my_scenes(user, request.version), many=True)
375377
return JsonResponse(serializer.data, safe=False)
376378

377379

@@ -470,7 +472,8 @@ def user_profile(request):
470472
- Shows scenes that the user has permissions to edit and a button to edit them.
471473
- Handles account deletes.
472474
"""
473-
if request.version not in TOPIC_SUPPORTED_API_VERSIONS:
475+
version = "v1" # TODO (mwfarb): resolve missing request.version
476+
if version not in TOPIC_SUPPORTED_API_VERSIONS:
474477
return redirect(f"/{TOPIC_SUPPORTED_API_VERSIONS[0]}/user_profile/")
475478

476479
if request.method == 'POST':
@@ -507,7 +510,7 @@ def user_profile(request):
507510
except User.DoesNotExist:
508511
messages.error(request, "Unable to complete account delete.")
509512

510-
scenes = get_my_scenes(request.user, request.version)
513+
scenes = get_my_scenes(request.user, version)
511514
devices = get_my_devices(request.user)
512515
staff = None
513516
if request.user.is_staff: # admin/staff
@@ -696,28 +699,30 @@ def arena_token(request):
696699
# produce nonce with 32-bits secure randomness
697700
nonce = f"{secrets.randbits(32):010d}"
698701
# define user object_ids server-side to prevent spoofing
699-
userid = camid = handleftid = handrightid = None
702+
ids = None
700703
if _field_requested(request, "userid"):
701704
userid = f"{nonce}_{username}"
702-
if _field_requested(request, "camid"):
703-
camid = f"{nonce}_{username}"
704-
if _field_requested(request, "handleftid"):
705-
handleftid = f"handLeft_{nonce}_{username}"
706-
if _field_requested(request, "handrightid"):
707-
handrightid = f"handRight_{nonce}_{username}"
705+
ids = {}
706+
ids["userid"] = userid
707+
if request.version == API_V2:
708+
ids["camid"] = userid # v2
709+
else:
710+
ids["camid"] = f"camera_{userid}" # v1
711+
ids["handleftid"] = f"handLeft_{userid}"
712+
ids["handrightid"] = f"handRight_{userid}"
713+
708714
if user.is_authenticated:
709715
duration = datetime.timedelta(days=1)
710716
else:
711717
duration = datetime.timedelta(hours=6)
712718
token = generate_arena_token(
713719
user=user,
714720
username=username,
715-
realm=request.POST.get("realm", "realm"),
721+
# TODO: realm cannot contain any /
722+
realm=request.POST.get("realm", None),
723+
# TODO: scene can/must contain one /
716724
ns_scene=request.POST.get("scene", None),
717-
camid=camid,
718-
userid=userid,
719-
handleftid=handleftid,
720-
handrightid=handrightid,
725+
ids=ids,
721726
duration=duration
722727
)
723728
if not token:
@@ -727,16 +732,8 @@ def arena_token(request):
727732
data = {
728733
"username": username,
729734
"token": token,
730-
"ids": {},
735+
"ids": ids,
731736
}
732-
if userid:
733-
data["ids"]["userid"] = userid
734-
if camid:
735-
data["ids"]["camid"] = camid
736-
if handleftid:
737-
data["ids"]["handleftid"] = handleftid
738-
if handrightid:
739-
data["ids"]["handrightid"] = handrightid
740737
response = HttpResponse(json.dumps(data), content_type="application/json")
741738
response.set_cookie(
742739
"mqtt_token",

0 commit comments

Comments
 (0)