epicyon/roles.py

324 lines
11 KiB
Python
Raw Normal View History

2020-04-04 10:28:58 +00:00
__filename__ = "roles.py"
__author__ = "Bob Mottram"
__license__ = "AGPL3+"
__version__ = "1.1.0"
__maintainer__ = "Bob Mottram"
__email__ = "bob@freedombone.net"
__status__ = "Production"
2019-07-18 15:09:23 +00:00
import os
from webfinger import webfingerHandle
from auth import createBasicAuthHeader
from posts import getPersonBox
from session import postJson
from utils import getNicknameFromActor
from utils import getDomainFromActor
2019-10-22 11:55:06 +00:00
from utils import loadJson
from utils import saveJson
2019-07-18 15:09:23 +00:00
2020-04-04 10:28:58 +00:00
2019-08-12 21:20:47 +00:00
def clearModeratorStatus(baseDir: str) -> None:
"""Removes moderator status from all accounts
This could be slow if there are many users, but only happens
rarely when moderators are appointed or removed
"""
2020-04-04 10:28:58 +00:00
directory = os.fsencode(baseDir + '/accounts/')
2019-09-27 12:09:04 +00:00
for f in os.scandir(directory):
2020-04-04 10:28:58 +00:00
f = f.name
filename = os.fsdecode(f)
2020-03-22 21:16:02 +00:00
if filename.endswith(".json") and '@' in filename:
2020-04-04 10:28:58 +00:00
filename = os.path.join(baseDir + '/accounts/', filename)
2019-08-12 21:20:47 +00:00
if '"moderator"' in open(filename).read():
2020-04-04 10:28:58 +00:00
actorJson = loadJson(filename)
2019-09-30 22:39:02 +00:00
if actorJson:
2019-08-12 21:20:47 +00:00
if actorJson['roles'].get('instance'):
if 'moderator' in actorJson['roles']['instance']:
actorJson['roles']['instance'].remove('moderator')
2020-04-04 10:28:58 +00:00
saveJson(actorJson, filename)
2019-08-12 21:20:47 +00:00
2020-04-04 10:28:58 +00:00
def addModerator(baseDir: str, nickname: str, domain: str) -> None:
2019-08-11 11:25:27 +00:00
"""Adds a moderator nickname to the file
"""
2019-08-12 21:20:47 +00:00
if ':' in domain:
2020-04-04 10:28:58 +00:00
domain = domain.split(':')[0]
moderatorsFile = baseDir + '/accounts/moderators.txt'
2019-08-11 11:25:27 +00:00
if os.path.isfile(moderatorsFile):
# is this nickname already in the file?
with open(moderatorsFile, "r") as f:
2020-04-04 10:28:58 +00:00
lines = f.readlines()
2019-08-11 11:25:27 +00:00
for moderator in lines:
2020-04-04 10:28:58 +00:00
moderator = moderator.strip('\n')
if moderator == nickname:
2019-08-11 11:25:27 +00:00
return
lines.append(nickname)
with open(moderatorsFile, "w") as f:
for moderator in lines:
2020-04-04 10:28:58 +00:00
moderator = moderator.strip('\n')
if len(moderator) > 1:
if os.path.isdir(baseDir + '/accounts/' +
moderator + '@' + domain):
f.write(moderator + '\n')
else:
with open(moderatorsFile, "w+") as f:
2020-04-04 10:28:58 +00:00
if os.path.isdir(baseDir + '/accounts/' +
nickname + '@' + domain):
f.write(nickname + '\n')
2020-03-22 21:16:02 +00:00
2020-04-04 10:28:58 +00:00
def removeModerator(baseDir: str, nickname: str):
2019-08-11 11:25:27 +00:00
"""Removes a moderator nickname from the file
"""
2020-04-04 10:28:58 +00:00
moderatorsFile = baseDir + '/accounts/moderators.txt'
2019-08-11 11:25:27 +00:00
if not os.path.isfile(moderatorsFile):
return
with open(moderatorsFile, "r") as f:
2020-04-04 10:28:58 +00:00
lines = f.readlines()
2019-08-11 11:25:27 +00:00
with open(moderatorsFile, "w") as f:
for moderator in lines:
2020-04-04 10:28:58 +00:00
moderator = moderator.strip('\n')
if len(moderator) > 1 and moderator != nickname:
f.write(moderator + '\n')
2019-08-11 11:25:27 +00:00
2020-04-04 10:28:58 +00:00
def setRole(baseDir: str, nickname: str, domain: str,
project: str, role: str) -> bool:
2019-07-18 15:09:23 +00:00
"""Set a person's role within a project
Setting the role to an empty string or None will remove it
"""
# avoid giant strings
2020-04-04 10:28:58 +00:00
if len(role) > 128 or len(project) > 128:
2019-07-18 15:09:23 +00:00
return False
2020-04-04 10:28:58 +00:00
actorFilename = baseDir + '/accounts/' + \
nickname + '@' + domain + '.json'
2019-07-18 15:09:23 +00:00
if not os.path.isfile(actorFilename):
return False
2019-09-30 22:39:02 +00:00
2020-04-04 10:28:58 +00:00
actorJson = loadJson(actorFilename)
2020-03-22 21:16:02 +00:00
if actorJson:
2019-07-18 15:09:23 +00:00
if role:
2019-08-11 11:25:27 +00:00
# add the role
2020-04-04 10:28:58 +00:00
if project == 'instance' and 'role' == 'moderator':
addModerator(baseDir, nickname, domain)
2019-07-18 15:09:23 +00:00
if actorJson['roles'].get(project):
if role not in actorJson['roles'][project]:
actorJson['roles'][project].append(role)
else:
2020-04-04 10:28:58 +00:00
actorJson['roles'][project] = [role]
2019-07-18 15:09:23 +00:00
else:
2019-08-11 11:25:27 +00:00
# remove the role
2020-04-04 10:28:58 +00:00
if project == 'instance':
removeModerator(baseDir, nickname)
2019-07-18 15:09:23 +00:00
if actorJson['roles'].get(project):
actorJson['roles'][project].remove(role)
# if the project contains no roles then remove it
2020-04-04 10:28:58 +00:00
if len(actorJson['roles'][project]) == 0:
2019-07-18 15:09:23 +00:00
del actorJson['roles'][project]
2020-04-04 10:28:58 +00:00
saveJson(actorJson, actorFilename)
2019-07-18 15:09:23 +00:00
return True
2020-04-04 10:28:58 +00:00
def getRoles(baseDir: str, nickname: str, domain: str,
2019-07-18 15:09:23 +00:00
project: str) -> []:
"""Returns the roles for a given person on a given project
"""
2020-04-04 10:28:58 +00:00
actorFilename = baseDir + '/accounts/' + \
nickname + '@' + domain + '.json'
2019-07-18 15:09:23 +00:00
if not os.path.isfile(actorFilename):
return False
2019-09-30 22:39:02 +00:00
2020-04-04 10:28:58 +00:00
actorJson = loadJson(actorFilename)
2019-09-30 22:39:02 +00:00
if actorJson:
2019-07-18 15:09:23 +00:00
if not actorJson.get('roles'):
return None
if not actorJson['roles'].get(project):
return None
return actorJson['roles'][project]
return None
2020-04-04 10:28:58 +00:00
def outboxDelegate(baseDir: str, authenticatedNickname: str,
messageJson: {}, debug: bool) -> bool:
2019-07-18 15:09:23 +00:00
"""Handles receiving a delegation request
"""
if not messageJson.get('type'):
2019-07-18 16:21:26 +00:00
return False
2020-04-04 10:28:58 +00:00
if not messageJson['type'] == 'Delegate':
2019-07-18 16:21:26 +00:00
return False
2019-07-18 15:09:23 +00:00
if not messageJson.get('object'):
2019-07-18 16:21:26 +00:00
return False
2019-07-18 15:09:23 +00:00
if not isinstance(messageJson['object'], dict):
2019-07-18 16:21:26 +00:00
return False
2019-07-18 15:09:23 +00:00
if not messageJson['object'].get('type'):
2019-07-18 16:21:26 +00:00
return False
2020-04-04 10:28:58 +00:00
if not messageJson['object']['type'] == 'Role':
2019-07-18 16:21:26 +00:00
return False
2019-07-18 15:09:23 +00:00
if not messageJson['object'].get('object'):
2019-07-18 16:21:26 +00:00
return False
2019-07-18 15:09:23 +00:00
if not messageJson['object'].get('actor'):
2019-07-18 16:21:26 +00:00
return False
2019-07-18 15:09:23 +00:00
if not isinstance(messageJson['object']['object'], str):
2019-07-18 16:21:26 +00:00
return False
2019-07-18 15:09:23 +00:00
if ';' not in messageJson['object']['object']:
print('WARN: No ; separator between project and role')
2019-07-18 16:21:26 +00:00
return False
2019-07-18 15:09:23 +00:00
2020-04-04 10:28:58 +00:00
delegatorNickname = getNicknameFromActor(messageJson['actor'])
if delegatorNickname != authenticatedNickname:
2019-07-19 10:01:24 +00:00
return
2020-04-04 10:28:58 +00:00
domain, port = getDomainFromActor(messageJson['actor'])
project = messageJson['object']['object'].split(';')[0].strip()
2019-07-18 15:09:23 +00:00
2019-07-18 16:21:26 +00:00
# instance delegators can delagate to other projects
# than their own
2020-04-04 10:28:58 +00:00
canDelegate = False
delegatorRoles = getRoles(baseDir, delegatorNickname,
domain, 'instance')
2019-07-18 15:09:23 +00:00
if delegatorRoles:
2019-07-18 16:21:26 +00:00
if 'delegator' in delegatorRoles:
2020-04-04 10:28:58 +00:00
canDelegate = True
2019-07-18 16:21:26 +00:00
2020-04-04 10:28:58 +00:00
if not canDelegate:
canDelegate = True
2019-07-18 16:21:26 +00:00
# non-instance delegators can only delegate within their project
2020-04-04 10:28:58 +00:00
delegatorRoles = getRoles(baseDir, delegatorNickname,
domain, project)
2019-07-18 16:21:26 +00:00
if delegatorRoles:
2019-07-18 15:09:23 +00:00
if 'delegator' not in delegatorRoles:
2019-07-18 16:21:26 +00:00
return False
else:
return False
2020-04-04 10:28:58 +00:00
if not canDelegate:
2019-07-18 16:21:26 +00:00
return False
2020-04-04 10:28:58 +00:00
nickname = getNicknameFromActor(messageJson['object']['actor'])
2019-09-02 09:43:43 +00:00
if not nickname:
2020-04-04 10:28:58 +00:00
print('WARN: unable to find nickname in ' +
messageJson['object']['actor'])
2019-09-02 09:43:43 +00:00
return False
2020-04-04 10:28:58 +00:00
role = \
messageJson['object']['object'].split(';')[1].strip().lower()
2019-07-18 15:09:23 +00:00
2019-07-18 16:43:48 +00:00
if not role:
2020-04-04 10:28:58 +00:00
setRole(baseDir, nickname, domain, project, None)
2019-07-18 16:43:48 +00:00
return True
2020-03-22 21:16:02 +00:00
2019-07-18 15:09:23 +00:00
# what roles is this person already assigned to?
2020-04-04 10:28:58 +00:00
existingRoles = getRoles(baseDir, nickname, domain, project)
2019-07-18 15:09:23 +00:00
if existingRoles:
if role in existingRoles:
2019-07-18 16:21:26 +00:00
if debug:
2020-04-04 10:28:58 +00:00
print(nickname + '@' + domain +
' is already assigned to the role ' +
role + ' within the project ' + project)
2019-07-18 16:21:26 +00:00
return False
2020-04-04 10:28:58 +00:00
setRole(baseDir, nickname, domain, project, role)
2019-07-18 16:21:26 +00:00
if debug:
2020-04-04 10:28:58 +00:00
print(nickname + '@' + domain +
' assigned to the role ' + role +
' within the project ' + project)
2019-07-18 16:21:26 +00:00
return True
2019-07-18 15:09:23 +00:00
2020-04-04 10:28:58 +00:00
def sendRoleViaServer(baseDir: str, session,
delegatorNickname: str, password: str,
delegatorDomain: str, delegatorPort: int,
httpPrefix: str, nickname: str,
project: str, role: str,
cachedWebfingers: {}, personCache: {},
debug: bool, projectVersion: str) -> {}:
2019-07-18 15:09:23 +00:00
"""A delegator creates a role for a person via c2s
2019-07-18 16:43:48 +00:00
Setting role to an empty string or None removes the role
2019-07-18 15:09:23 +00:00
"""
if not session:
print('WARN: No session for sendRoleViaServer')
return 6
2020-04-04 10:28:58 +00:00
delegatorDomainFull = delegatorDomain
if delegatorPort:
if delegatorPort != 80 and delegatorPort != 443:
if ':' not in delegatorDomain:
2020-04-04 10:28:58 +00:00
delegatorDomainFull = \
delegatorDomain + ':' + str(delegatorPort)
2020-03-22 21:16:02 +00:00
2020-04-04 10:28:58 +00:00
toUrl = \
httpPrefix + '://' + delegatorDomainFull + '/users/' + nickname
ccUrl = \
httpPrefix + '://' + delegatorDomainFull + '/users/' + \
delegatorNickname + '/followers'
2019-07-18 15:09:23 +00:00
2019-07-18 16:43:48 +00:00
if role:
2020-04-04 10:28:58 +00:00
roleStr = project.lower() + ';' + role.lower()
2019-07-18 16:43:48 +00:00
else:
2020-04-04 10:28:58 +00:00
roleStr = project.lower() + ';'
actor = \
httpPrefix + '://' + delegatorDomainFull + \
'/users/' + delegatorNickname
delegateActor = \
httpPrefix + '://' + delegatorDomainFull + '/users/' + nickname
newRoleJson = {
2019-07-18 15:09:23 +00:00
'type': 'Delegate',
2020-04-04 10:28:58 +00:00
'actor': actor,
2019-07-18 15:09:23 +00:00
'object': {
'type': 'Role',
2020-04-04 10:28:58 +00:00
'actor': delegateActor,
2019-07-18 16:43:48 +00:00
'object': roleStr,
2019-07-18 15:09:23 +00:00
'to': [toUrl],
2020-03-22 21:16:02 +00:00
'cc': [ccUrl]
2019-07-18 15:09:23 +00:00
},
'to': [toUrl],
'cc': [ccUrl]
}
2020-04-04 10:28:58 +00:00
handle = \
httpPrefix + '://' + delegatorDomainFull + '/@' + delegatorNickname
2019-07-18 15:09:23 +00:00
# lookup the inbox for the To handle
2020-04-04 10:28:58 +00:00
wfRequest = webfingerHandle(session, handle, httpPrefix,
cachedWebfingers,
delegatorDomain, projectVersion)
2019-07-18 15:09:23 +00:00
if not wfRequest:
if debug:
2020-04-04 10:28:58 +00:00
print('DEBUG: announce webfinger failed for ' + handle)
2019-07-18 15:09:23 +00:00
return 1
2020-04-04 10:28:58 +00:00
postToBox = 'outbox'
2019-07-18 15:09:23 +00:00
# get the actor inbox for the To handle
2020-04-04 10:28:58 +00:00
(inboxUrl, pubKeyId, pubKey,
fromPersonId, sharedInbox,
capabilityAcquisition,
avatarUrl, displayName) = getPersonBox(baseDir, session,
wfRequest, personCache,
projectVersion, httpPrefix,
delegatorNickname,
delegatorDomain, postToBox)
2020-03-22 21:16:02 +00:00
2019-07-18 15:09:23 +00:00
if not inboxUrl:
if debug:
2020-04-04 10:28:58 +00:00
print('DEBUG: No ' + postToBox + ' was found for ' + handle)
2019-07-18 15:09:23 +00:00
return 3
if not fromPersonId:
if debug:
2020-04-04 10:28:58 +00:00
print('DEBUG: No actor was found for ' + handle)
2019-07-18 15:09:23 +00:00
return 4
2020-03-22 21:16:02 +00:00
2020-04-04 10:28:58 +00:00
authHeader = createBasicAuthHeader(delegatorNickname, password)
2020-03-22 21:16:02 +00:00
2020-04-04 10:28:58 +00:00
headers = {
'host': delegatorDomain,
'Content-type': 'application/json',
2020-03-22 20:36:19 +00:00
'Authorization': authHeader
}
2020-04-04 10:28:58 +00:00
postResult = \
postJson(session, newRoleJson, [], inboxUrl, headers, "inbox:write")
if not postResult:
if debug:
print('DEBUG: POST announce failed for c2s to '+inboxUrl)
# return 5
2019-07-18 15:09:23 +00:00
if debug:
print('DEBUG: c2s POST role success')
return newRoleJson