From 958e76ea8951e35443d7e4a49cad5ddde21bdf60 Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Thu, 18 Jul 2019 16:09:23 +0100 Subject: [PATCH] Functions for role delegation --- daemon.py | 7 +- epicyon.py | 3 +- person.py | 46 +------------ roles.py | 195 +++++++++++++++++++++++++++++++++++++++++++++++++++++ tests.py | 2 +- 5 files changed, 205 insertions(+), 48 deletions(-) create mode 100644 roles.py diff --git a/daemon.py b/daemon.py index 5aa9ae31..9daca029 100644 --- a/daemon.py +++ b/daemon.py @@ -41,6 +41,7 @@ from like import outboxUndoLike from blocking import outboxBlock from blocking import outboxUndoBlock from config import setConfigParam +from roles import outboxDelegate import os import sys @@ -172,7 +173,8 @@ class PubServer(BaseHTTPRequestHandler): permittedOutboxTypes=[ 'Create','Announce','Like','Follow','Undo', \ - 'Update','Add','Remove','Block','Delete' + 'Update','Add','Remove','Block','Delete', \ + 'Delegate' ] if messageJson['type'] not in permittedOutboxTypes: if self.server.debug: @@ -218,6 +220,9 @@ class PubServer(BaseHTTPRequestHandler): if self.server.debug: print('DEBUG: handle any unfollow requests') outboxUndoFollow(self.server.baseDir,messageJson,self.server.debug) + if self.server.debug: + print('DEBUG: handle delegation requests') + outboxDelegate(self.server.baseDir,messageJson,self.server.debug) if self.server.debug: print('DEBUG: handle any like requests') outboxLike(self.server.baseDir,self.server.httpPrefix, \ diff --git a/epicyon.py b/epicyon.py index b0901503..1a8674a3 100644 --- a/epicyon.py +++ b/epicyon.py @@ -15,7 +15,7 @@ from person import setBio from person import validNickname from person import setProfileImage from person import setSkillLevel -from person import setRole +from roles import setRole from person import setAvailability from person import setOrganizationScheme from webfinger import webfingerHandle @@ -67,6 +67,7 @@ from like import sendLikeViaServer from like import sendUndoLikeViaServer from blocking import sendBlockViaServer from blocking import sendUndoBlockViaServer +from roles import sendRoleViaServer import argparse def str2bool(v): diff --git a/person.py b/person.py index 074ce1c8..ae76a055 100644 --- a/person.py +++ b/person.py @@ -19,6 +19,7 @@ from webfinger import createWebfingerEndpoint from webfinger import storeWebfingerEndpoint from posts import createOutbox from auth import storeBasicCredentials +from roles import setRole def generateRSAKey() -> (str,str): key = RSA.generate(2048) @@ -108,51 +109,6 @@ def setSkillLevel(baseDir: str,nickname: str,domain: str, \ commentjson.dump(actorJson, fp, indent=4, sort_keys=False) return True -def getRoles(baseDir: str,nickname: str,domain: str, \ - project: str) -> []: - """Returns the roles for a given person on a given project - """ - actorFilename=baseDir+'/accounts/'+nickname+'@'+domain+'.json' - if not os.path.isfile(actorFilename): - return False - with open(actorFilename, 'r') as fp: - actorJson=commentjson.load(fp) - if not actorJson.get('roles'): - return None - if not actorJson['roles'].get(project): - return None - return actorJson['roles'][project] - return None - -def setRole(baseDir: str,nickname: str,domain: str, \ - project: str,role: str) -> bool: - """Set a person's role within a project - Setting the role to an empty string or None will remove it - """ - # avoid giant strings - if len(role)>128 or len(project)>128: - return False - actorFilename=baseDir+'/accounts/'+nickname+'@'+domain+'.json' - if not os.path.isfile(actorFilename): - return False - with open(actorFilename, 'r') as fp: - actorJson=commentjson.load(fp) - if role: - if actorJson['roles'].get(project): - if role not in actorJson['roles'][project]: - actorJson['roles'][project].append(role) - else: - actorJson['roles'][project]=[role] - else: - if actorJson['roles'].get(project): - actorJson['roles'][project].remove(role) - # if the project contains no roles then remove it - if len(actorJson['roles'][project])==0: - del actorJson['roles'][project] - with open(actorFilename, 'w') as fp: - commentjson.dump(actorJson, fp, indent=4, sort_keys=False) - return True - def setOrganizationScheme(baseDir: str,nickname: str,domain: str, \ schema: str) -> bool: """Set the organization schema within which a person exists diff --git a/roles.py b/roles.py new file mode 100644 index 00000000..e5b43922 --- /dev/null +++ b/roles.py @@ -0,0 +1,195 @@ +__filename__ = "roles.py" +__author__ = "Bob Mottram" +__license__ = "AGPL3+" +__version__ = "0.0.1" +__maintainer__ = "Bob Mottram" +__email__ = "bob@freedombone.net" +__status__ = "Production" + +import json +import commentjson +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 + +def setRole(baseDir: str,nickname: str,domain: str, \ + project: str,role: str) -> bool: + """Set a person's role within a project + Setting the role to an empty string or None will remove it + """ + # avoid giant strings + if len(role)>128 or len(project)>128: + return False + actorFilename=baseDir+'/accounts/'+nickname+'@'+domain+'.json' + if not os.path.isfile(actorFilename): + return False + with open(actorFilename, 'r') as fp: + actorJson=commentjson.load(fp) + if role: + if actorJson['roles'].get(project): + if role not in actorJson['roles'][project]: + actorJson['roles'][project].append(role) + else: + actorJson['roles'][project]=[role] + else: + if actorJson['roles'].get(project): + actorJson['roles'][project].remove(role) + # if the project contains no roles then remove it + if len(actorJson['roles'][project])==0: + del actorJson['roles'][project] + with open(actorFilename, 'w') as fp: + commentjson.dump(actorJson, fp, indent=4, sort_keys=False) + return True + +def getRoles(baseDir: str,nickname: str,domain: str, \ + project: str) -> []: + """Returns the roles for a given person on a given project + """ + actorFilename=baseDir+'/accounts/'+nickname+'@'+domain+'.json' + if not os.path.isfile(actorFilename): + return False + with open(actorFilename, 'r') as fp: + actorJson=commentjson.load(fp) + if not actorJson.get('roles'): + return None + if not actorJson['roles'].get(project): + return None + return actorJson['roles'][project] + return None + +def outboxDelegate(baseDir: str,messageJson: {},debug: bool) -> None: + """Handles receiving a delegation request + """ + if not messageJson.get('type'): + return + if not messageJson['type']=='Delegate': + return + if not messageJson.get('object'): + return + if not isinstance(messageJson['object'], dict): + return + if not messageJson['object'].get('type'): + return + if not messageJson['object']['type']=='Role': + return + if not messageJson['object'].get('object'): + return + if not messageJson['object'].get('actor'): + return + if not isinstance(messageJson['object']['object'], str): + return + if ';' not in messageJson['object']['object']: + print('WARN: No ; separator between project and role') + return + if debug: + print('DEBUG: delegate activity arrived in outbox') + + delegatorNickname=getNicknameFromActor(messageJson['actor']) + domain,port=getDomainFromActor(messageJson['actor']) + project=messageJson['object']['object'].split(';')[0].strip() + + # does the delegator have capability to delegate in this project? + delegatorRoles=getRoles(baseDir,delegatorNickname, \ + domain,project) + if delegatorRoles: + if 'delegator' not in delegatorRoles: + # instance delegators can delagate to other projects + # than their own + delegatorRoles=getRoles(baseDir,delegatorNickname, \ + domain,'instance') + if 'delegator' not in delegatorRoles: + return + + nickname=getNicknameFromActor(messageJson['object']['actor']) + domainFull=domain + if port: + if port!=80 and port!=443: + domainFull=domain+':'+str(port) + role=messageJson['object']['object'].split(';')[1].strip().lower() + + # what roles is this person already assigned to? + existingRoles=getRoles(baseDir,nickname,domain,project) + if existingRoles: + if role in existingRoles: + print(nickname+'@'+domain+' is already assigned to the role '+role+' within the project '+project) + return + setRole(baseDir,nickname,domain,project,role) + print(nickname+'@'+domain+' assigned to the role '+role+' within the project '+project) + +def sendRoleViaServer(session,delegatorNickname: str,password: str, + delegatorDomain: str,delegatorPort: int, \ + httpPrefix: str,nickname: str, \ + project: str,role: str, \ + cachedWebfingers: {},personCache: {}, \ + debug: bool) -> {}: + """A delegator creates a role for a person via c2s + """ + if not session: + print('WARN: No session for sendRoleViaServer') + return 6 + + delegatorDomainFull=delegatorDomain + if fromPort!=80 and fromPort!=443: + delegatorDomainFull=delegatorDomain+':'+str(fromPort) + + toUrl = httpPrefix+'://'+delegatorDomainFull+'/users/'+nickname + ccUrl = httpPrefix+'://'+delegatorDomainFull+'/users/'+delegatorNickname+'/followers' + + newRoleJson = { + 'type': 'Delegate', + 'actor': httpPrefix+'://'+delegatorDomainFull+'/users/'+delegatorNickname, + 'object': { + 'type': 'Role', + 'actor': httpPrefix+'://'+delegatorDomainFull+'/users/'+nickname, + 'object': project.lower()+';'+role.lower(), + 'to': [toUrl], + 'cc': [ccUrl] + }, + 'to': [toUrl], + 'cc': [ccUrl] + } + + handle=httpPrefix+'://'+delegatorDomainFull+'/@'+delegatorNickname + + # lookup the inbox for the To handle + wfRequest = webfingerHandle(session,handle,httpPrefix,cachedWebfingers) + if not wfRequest: + if debug: + print('DEBUG: announce webfinger failed for '+handle) + return 1 + + postToBox='outbox' + + # get the actor inbox for the To handle + inboxUrl,pubKeyId,pubKey,fromPersonId,sharedInbox,capabilityAcquisition = \ + getPersonBox(session,wfRequest,personCache,postToBox) + + if not inboxUrl: + if debug: + print('DEBUG: No '+postToBox+' was found for '+handle) + return 3 + if not fromPersonId: + if debug: + print('DEBUG: No actor was found for '+handle) + return 4 + + authHeader=createBasicAuthHeader(delegatorNickname,password) + + headers = {'host': delegatorDomain, \ + 'Content-type': 'application/json', \ + 'Authorization': authHeader} + postResult = \ + postJson(session,newRoleJson,[],inboxUrl,headers,"inbox:write") + #if not postResult: + # if debug: + # print('DEBUG: POST announce failed for c2s to '+inboxUrl) + # return 5 + + if debug: + print('DEBUG: c2s POST role success') + + return newRoleJson diff --git a/tests.py b/tests.py index f6edba79..b893d3be 100644 --- a/tests.py +++ b/tests.py @@ -44,7 +44,7 @@ from person import createPerson from person import setPreferredNickname from person import setBio from person import setSkillLevel -from person import setRole +from roles import setRole from auth import createBasicAuthHeader from auth import authorizeBasic from auth import storeBasicCredentials