roles.py 11 KB

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