roles.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327
  1. __filename__ = "roles.py"
  2. __author__ = "Bob Mottram"
  3. __license__ = "AGPL3+"
  4. __version__ = "1.1.0"
  5. __maintainer__ = "Bob Mottram"
  6. __email__ = "bob@freedombone.net"
  7. __status__ = "Production"
  8. import os
  9. from webfinger import webfingerHandle
  10. from auth import createBasicAuthHeader
  11. from posts import getPersonBox
  12. from session import postJson
  13. from utils import getNicknameFromActor
  14. from utils import getDomainFromActor
  15. from utils import loadJson
  16. from utils import saveJson
  17. def clearModeratorStatus(baseDir: str) -> None:
  18. """Removes moderator status from all accounts
  19. This could be slow if there are many users, but only happens
  20. rarely when moderators are appointed or removed
  21. """
  22. directory = os.fsencode(baseDir + '/accounts/')
  23. for f in os.scandir(directory):
  24. f = f.name
  25. filename = os.fsdecode(f)
  26. if filename.endswith(".json") and '@' in filename:
  27. filename = os.path.join(baseDir + '/accounts/', filename)
  28. if '"moderator"' in open(filename).read():
  29. actorJson = loadJson(filename)
  30. if actorJson:
  31. if actorJson['roles'].get('instance'):
  32. if 'moderator' in actorJson['roles']['instance']:
  33. actorJson['roles']['instance'].remove('moderator')
  34. saveJson(actorJson, filename)
  35. def addModerator(baseDir: str, nickname: str, domain: str) -> None:
  36. """Adds a moderator nickname to the file
  37. """
  38. if ':' in domain:
  39. domain = domain.split(':')[0]
  40. moderatorsFile = baseDir + '/accounts/moderators.txt'
  41. if os.path.isfile(moderatorsFile):
  42. # is this nickname already in the file?
  43. with open(moderatorsFile, "r") as f:
  44. lines = f.readlines()
  45. for moderator in lines:
  46. moderator = moderator.strip('\n').strip('\r')
  47. if moderator == nickname:
  48. return
  49. lines.append(nickname)
  50. with open(moderatorsFile, "w") as f:
  51. for moderator in lines:
  52. moderator = moderator.strip('\n').strip('\r')
  53. if len(moderator) > 1:
  54. if os.path.isdir(baseDir + '/accounts/' +
  55. moderator + '@' + domain):
  56. f.write(moderator + '\n')
  57. else:
  58. with open(moderatorsFile, "w+") as f:
  59. if os.path.isdir(baseDir + '/accounts/' +
  60. nickname + '@' + domain):
  61. f.write(nickname + '\n')
  62. def removeModerator(baseDir: str, nickname: str):
  63. """Removes a moderator nickname from the file
  64. """
  65. moderatorsFile = baseDir + '/accounts/moderators.txt'
  66. if not os.path.isfile(moderatorsFile):
  67. return
  68. with open(moderatorsFile, "r") as f:
  69. lines = f.readlines()
  70. with open(moderatorsFile, "w") as f:
  71. for moderator in lines:
  72. moderator = moderator.strip('\n').strip('\r')
  73. if len(moderator) > 1 and moderator != nickname:
  74. f.write(moderator + '\n')
  75. def setRole(baseDir: str, nickname: str, domain: str,
  76. project: str, role: str) -> bool:
  77. """Set a person's role within a project
  78. Setting the role to an empty string or None will remove it
  79. """
  80. # avoid giant strings
  81. if len(role) > 128 or len(project) > 128:
  82. return False
  83. actorFilename = baseDir + '/accounts/' + \
  84. nickname + '@' + domain + '.json'
  85. if not os.path.isfile(actorFilename):
  86. return False
  87. actorJson = loadJson(actorFilename)
  88. if actorJson:
  89. if role:
  90. # add the role
  91. if project == 'instance' and 'role' == 'moderator':
  92. addModerator(baseDir, nickname, domain)
  93. if actorJson['roles'].get(project):
  94. if role not in actorJson['roles'][project]:
  95. actorJson['roles'][project].append(role)
  96. else:
  97. actorJson['roles'][project] = [role]
  98. else:
  99. # remove the role
  100. if project == 'instance':
  101. removeModerator(baseDir, nickname)
  102. if actorJson['roles'].get(project):
  103. actorJson['roles'][project].remove(role)
  104. # if the project contains no roles then remove it
  105. if len(actorJson['roles'][project]) == 0:
  106. del actorJson['roles'][project]
  107. saveJson(actorJson, actorFilename)
  108. return True
  109. def getRoles(baseDir: str, nickname: str, domain: str,
  110. project: str) -> []:
  111. """Returns the roles for a given person on a given project
  112. """
  113. actorFilename = baseDir + '/accounts/' + \
  114. nickname + '@' + domain + '.json'
  115. if not os.path.isfile(actorFilename):
  116. return False
  117. actorJson = loadJson(actorFilename)
  118. if actorJson:
  119. if not actorJson.get('roles'):
  120. return None
  121. if not actorJson['roles'].get(project):
  122. return None
  123. return actorJson['roles'][project]
  124. return None
  125. def outboxDelegate(baseDir: str, authenticatedNickname: str,
  126. messageJson: {}, debug: bool) -> bool:
  127. """Handles receiving a delegation request
  128. """
  129. if not messageJson.get('type'):
  130. return False
  131. if not messageJson['type'] == 'Delegate':
  132. return False
  133. if not messageJson.get('object'):
  134. return False
  135. if not isinstance(messageJson['object'], dict):
  136. return False
  137. if not messageJson['object'].get('type'):
  138. return False
  139. if not messageJson['object']['type'] == 'Role':
  140. return False
  141. if not messageJson['object'].get('object'):
  142. return False
  143. if not messageJson['object'].get('actor'):
  144. return False
  145. if not isinstance(messageJson['object']['object'], str):
  146. return False
  147. if ';' not in messageJson['object']['object']:
  148. print('WARN: No ; separator between project and role')
  149. return False
  150. delegatorNickname = getNicknameFromActor(messageJson['actor'])
  151. if delegatorNickname != authenticatedNickname:
  152. return
  153. domain, port = getDomainFromActor(messageJson['actor'])
  154. project = messageJson['object']['object'].split(';')[0].strip()
  155. # instance delegators can delagate to other projects
  156. # than their own
  157. canDelegate = False
  158. delegatorRoles = getRoles(baseDir, delegatorNickname,
  159. domain, 'instance')
  160. if delegatorRoles:
  161. if 'delegator' in delegatorRoles:
  162. canDelegate = True
  163. if not canDelegate:
  164. canDelegate = True
  165. # non-instance delegators can only delegate within their project
  166. delegatorRoles = getRoles(baseDir, delegatorNickname,
  167. domain, project)
  168. if delegatorRoles:
  169. if 'delegator' not in delegatorRoles:
  170. return False
  171. else:
  172. return False
  173. if not canDelegate:
  174. return False
  175. nickname = getNicknameFromActor(messageJson['object']['actor'])
  176. if not nickname:
  177. print('WARN: unable to find nickname in ' +
  178. messageJson['object']['actor'])
  179. return False
  180. role = \
  181. messageJson['object']['object'].split(';')[1].strip().lower()
  182. if not role:
  183. setRole(baseDir, nickname, domain, project, None)
  184. return True
  185. # what roles is this person already assigned to?
  186. existingRoles = getRoles(baseDir, nickname, domain, project)
  187. if existingRoles:
  188. if role in existingRoles:
  189. if debug:
  190. print(nickname + '@' + domain +
  191. ' is already assigned to the role ' +
  192. role + ' within the project ' + project)
  193. return False
  194. setRole(baseDir, nickname, domain, project, role)
  195. if debug:
  196. print(nickname + '@' + domain +
  197. ' assigned to the role ' + role +
  198. ' within the project ' + project)
  199. return True
  200. def sendRoleViaServer(baseDir: str, session,
  201. delegatorNickname: str, password: str,
  202. delegatorDomain: str, delegatorPort: int,
  203. httpPrefix: str, nickname: str,
  204. project: str, role: str,
  205. cachedWebfingers: {}, personCache: {},
  206. debug: bool, projectVersion: str) -> {}:
  207. """A delegator creates a role for a person via c2s
  208. Setting role to an empty string or None removes the role
  209. """
  210. if not session:
  211. print('WARN: No session for sendRoleViaServer')
  212. return 6
  213. delegatorDomainFull = delegatorDomain
  214. if delegatorPort:
  215. if delegatorPort != 80 and delegatorPort != 443:
  216. if ':' not in delegatorDomain:
  217. delegatorDomainFull = \
  218. delegatorDomain + ':' + str(delegatorPort)
  219. toUrl = \
  220. httpPrefix + '://' + delegatorDomainFull + '/users/' + nickname
  221. ccUrl = \
  222. httpPrefix + '://' + delegatorDomainFull + '/users/' + \
  223. delegatorNickname + '/followers'
  224. if role:
  225. roleStr = project.lower() + ';' + role.lower()
  226. else:
  227. roleStr = project.lower() + ';'
  228. actor = \
  229. httpPrefix + '://' + delegatorDomainFull + \
  230. '/users/' + delegatorNickname
  231. delegateActor = \
  232. httpPrefix + '://' + delegatorDomainFull + '/users/' + nickname
  233. newRoleJson = {
  234. 'type': 'Delegate',
  235. 'actor': actor,
  236. 'object': {
  237. 'type': 'Role',
  238. 'actor': delegateActor,
  239. 'object': roleStr,
  240. 'to': [toUrl],
  241. 'cc': [ccUrl]
  242. },
  243. 'to': [toUrl],
  244. 'cc': [ccUrl]
  245. }
  246. handle = \
  247. httpPrefix + '://' + delegatorDomainFull + '/@' + delegatorNickname
  248. # lookup the inbox for the To handle
  249. wfRequest = webfingerHandle(session, handle, httpPrefix,
  250. cachedWebfingers,
  251. delegatorDomain, projectVersion)
  252. if not wfRequest:
  253. if debug:
  254. print('DEBUG: announce webfinger failed for ' + handle)
  255. return 1
  256. if not isinstance(wfRequest, dict):
  257. print('WARN: Webfinger for ' + handle + ' did not return a dict. ' +
  258. str(wfRequest))
  259. return 1
  260. postToBox = 'outbox'
  261. # get the actor inbox for the To handle
  262. (inboxUrl, pubKeyId, pubKey,
  263. fromPersonId, sharedInbox,
  264. capabilityAcquisition,
  265. avatarUrl, displayName) = getPersonBox(baseDir, session,
  266. wfRequest, personCache,
  267. projectVersion, httpPrefix,
  268. delegatorNickname,
  269. delegatorDomain, postToBox)
  270. if not inboxUrl:
  271. if debug:
  272. print('DEBUG: No ' + postToBox + ' was found for ' + handle)
  273. return 3
  274. if not fromPersonId:
  275. if debug:
  276. print('DEBUG: No actor was found for ' + handle)
  277. return 4
  278. authHeader = createBasicAuthHeader(delegatorNickname, password)
  279. headers = {
  280. 'host': delegatorDomain,
  281. 'Content-type': 'application/json',
  282. 'Authorization': authHeader
  283. }
  284. postResult = \
  285. postJson(session, newRoleJson, [], inboxUrl, headers, "inbox:write")
  286. if not postResult:
  287. if debug:
  288. print('DEBUG: POST announce failed for c2s to '+inboxUrl)
  289. # return 5
  290. if debug:
  291. print('DEBUG: c2s POST role success')
  292. return newRoleJson