roles.py 11 KB

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