roles.py 11 KB

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