diff --git a/src/core/cs3iface.py b/src/core/cs3iface.py index 65f8f77b..a6eed5c8 100644 --- a/src/core/cs3iface.py +++ b/src/core/cs3iface.py @@ -86,7 +86,7 @@ def stat(endpoint, fileid, userid, versioninv=0): inode = urlsafe_b64encode(statInfo.info.id.opaque_id.encode()) return {'inode': statInfo.info.id.storage_id + '-' + inode.decode(), 'filepath': statInfo.info.path, - 'userid': statInfo.info.owner.opaque_id, + 'userid': statInfo.info.owner.opaque_id + '@' + statInfo.info.owner.idp, 'size': statInfo.info.size, 'mtime': statInfo.info.mtime.seconds } diff --git a/src/core/wopi.py b/src/core/wopi.py index 2a7ad2f2..040712cb 100644 --- a/src/core/wopi.py +++ b/src/core/wopi.py @@ -45,7 +45,7 @@ def checkFileInfo(fileid): # encode the path part as it is going to be an URL GET argument filemd['BreadcrumbFolderUrl'] = furl[:furl.find('=')+1] + url_quote_plus(furl[furl.find('=')+1:]) if furl != '/' else '' if acctok['username'] == '': - filemd['UserFriendlyName'] = 'Guest ' + utils.randomString(3) + filemd['UserFriendlyName'] = 'Guest ' + (acctok['wopiuser'][:3] if acctok.get('wopiuser') else utils.randomString(3)) if '?path' in furl and furl[-1] != '/' and furl[-1] != '=': # this is a subfolder of a public share, show it filemd['BreadcrumbFolderName'] = 'Back to ' + furl[furl.find('?path'):].split('/')[-1] @@ -61,7 +61,7 @@ def checkFileInfo(fileid): filemd['DownloadUrl'] = '%s?access_token=%s' % \ (srv.config.get('general', 'downloadurl'), flask.request.args['access_token']) filemd['OwnerId'] = statInfo['userid'] - filemd['UserId'] = acctok['userid'] # typically same as OwnerId; different when accessing shared documents + filemd['UserId'] = acctok['wopiuser'] if acctok.get('wopiuser') else acctok['userid'] # typically same as OwnerId; different when accessing shared documents filemd['Size'] = statInfo['size'] # TODO the version is generated like this in ownCloud: 'V' . $file->getEtag() . \md5($file->getChecksum()); filemd['Version'] = statInfo['mtime'] # mtime is used as version here @@ -319,10 +319,10 @@ def putRelative(fileid, reqheaders, acctok): log.info('msg="PutRelative: generating new access token" user="%s" filename="%s" ' \ 'mode="ViewMode.READ_WRITE" friendlyname="%s"' % (acctok['userid'][-20:], targetName, acctok['username'])) - inode, newacctok = utils.generateAccessToken(acctok['userid'], targetName, \ - utils.ViewMode.READ_WRITE, acctok['username'], \ - acctok['folderurl'], acctok['endpoint'], acctok['appname'], \ - acctok['appediturl'], acctok['appviewurl']) + inode, newacctok = utils.generateAccessToken(acctok['userid'], targetName, utils.ViewMode.READ_WRITE, + (acctok['username'], acctok['wopiuser']), \ + acctok['folderurl'], acctok['endpoint'], \ + (acctok['appname'], acctok['appediturl'], acctok['appviewurl'])) # prepare and send the response as JSON putrelmd = {} putrelmd['Name'] = os.path.basename(targetName) diff --git a/src/core/wopiutils.py b/src/core/wopiutils.py index 97ace286..45ede446 100644 --- a/src/core/wopiutils.py +++ b/src/core/wopiutils.py @@ -117,9 +117,11 @@ def randomString(size): return ''.join([choice(ascii_lowercase) for _ in range(size)]) -def generateAccessToken(userid, fileid, viewmode, username, folderurl, endpoint, appname, appediturl, appviewurl): +def generateAccessToken(userid, fileid, viewmode, user, folderurl, endpoint, app): '''Generates an access token for a given file and a given user, and returns a tuple with the file's inode and the URL-encoded access token.''' + appname, appediturl, appviewurl = app + username, wopiuser = user try: # stat the file to check for existence and get a version-invariant inode and modification time: # the inode serves as fileid (and must not change across save operations), the mtime is used for version information. @@ -139,13 +141,14 @@ def generateAccessToken(userid, fileid, viewmode, username, folderurl, endpoint, log.critical('msg="No app URLs registered for the given file type" fileext="%s" mimetypescount="%d"' % (fext, len(endpoints) if endpoints else 0)) raise IOError - acctok = jwt.encode({'userid': userid, 'filename': statinfo['filepath'], 'username': username, + acctok = jwt.encode({'userid': userid, 'wopiuser': wopiuser, 'filename': statinfo['filepath'], 'username': username, 'viewmode': viewmode.value, 'folderurl': folderurl, 'endpoint': endpoint, 'appname': appname, 'appediturl': appediturl, 'appviewurl': appviewurl, 'exp': exptime}, srv.wopisecret, algorithm='HS256') - log.info('msg="Access token generated" userid="%s" mode="%s" endpoint="%s" filename="%s" inode="%s" ' \ + log.info('msg="Access token generated" userid="%s" wopiuser="%s" mode="%s" endpoint="%s" filename="%s" inode="%s" ' \ 'mtime="%s" folderurl="%s" appname="%s" expiration="%d" token="%s"' % - (userid[-20:], viewmode, endpoint, statinfo['filepath'], statinfo['inode'], statinfo['mtime'], \ + (userid[-20:], wopiuser if wopiuser else username, viewmode, endpoint, \ + statinfo['filepath'], statinfo['inode'], statinfo['mtime'], \ folderurl, appname, exptime, acctok[-20:])) # return the inode == fileid, the filepath and the access token return statinfo['inode'], acctok diff --git a/src/wopiserver.py b/src/wopiserver.py index 0e92d6b0..7d01f200 100755 --- a/src/wopiserver.py +++ b/src/wopiserver.py @@ -217,7 +217,10 @@ def iopOpenInApp(): - string fileid: the Reva fileid of the file to be opened - string endpoint (optional): the storage endpoint to be used to look up the file or the storage id, in case of multi-instance underlying storage; defaults to 'default' - - string username (optional): user's full display name, typically shown by the Office app + - string username (optional): user's full display name, typically shown by the app; defaults to + 'Guest ' + 3 random letters to represent anonymous users + - string userid (optional): an unique identifier for the user, used internally by the app; defaults to + a random string of 10 characters to represent anonymous users - string folderurl (optional): the URL to come back to the containing folder for this file, typically shown by the app - string appname: the identifier of the end-user application to be served - string appurl: the URL of the end-user application @@ -239,7 +242,7 @@ def iopOpenInApp(): 'client="%s" clientAuth="%s"' % (req.remote_addr, req.headers.get('Authorization'))) return UNAUTHORIZED try: - userid = req.headers['TokenHeader'] + usertoken = req.headers['TokenHeader'] except KeyError: Wopi.log.warning('msg="iopOpenInApp: missing TokenHeader in request" client="%s"' % req.remote_addr) return UNAUTHORIZED @@ -256,6 +259,8 @@ def iopOpenInApp(): (req.remote_addr, req.args.get('viewmode'), e)) return 'Missing or invalid viewmode argument', http.client.BAD_REQUEST username = req.args.get('username', '') + # this needs to be a unique identifier: if missing (case of anonymous users), just generate a random string + wopiuser = req.args.get('userid', utils.randomString(10)) folderurl = url_unquote(req.args.get('folderurl', '%2F')) # defaults to `/` endpoint = req.args.get('endpoint', 'default') appname = url_unquote(req.args.get('appname', '')) @@ -277,12 +282,12 @@ def iopOpenInApp(): appurl = appviewurl = Wopi.wopiurl + '/wopi/bridge/open' try: - inode, acctok = utils.generateAccessToken(userid, fileid, viewmode, username, folderurl, endpoint, - appname, appurl, appviewurl) + inode, acctok = utils.generateAccessToken(usertoken, fileid, viewmode, (username, wopiuser), folderurl, endpoint, + (appname, appurl, appviewurl)) except IOError as e: Wopi.log.info('msg="iopOpenInApp: remote error on generating token" client="%s" user="%s" ' \ 'friendlyname="%s" mode="%s" endpoint="%s" reason="%s"' % - (req.remote_addr, userid[-20:], username, viewmode, endpoint, e)) + (req.remote_addr, usertoken[-20:], username, viewmode, endpoint, e)) return 'Remote error, file not found or file is a directory', http.client.NOT_FOUND res = {} @@ -540,16 +545,12 @@ def cboxOpen_deprecated(): return UNAUTHORIZED # now validate the user identity and deny root access try: - if 'TokenHeader' in req.headers: - userid = req.headers['TokenHeader'] - else: - # backwards compatibility - userid = 'N/A' - ruid = int(req.args['ruid']) - rgid = int(req.args['rgid']) - userid = '%d:%d' % (ruid, rgid) - if ruid == 0 or rgid == 0: - raise ValueError + userid = 'N/A' + ruid = int(req.args['ruid']) + rgid = int(req.args['rgid']) + userid = '%d:%d' % (ruid, rgid) + if ruid == 0 or rgid == 0: + raise ValueError except ValueError: Wopi.log.warning('msg="cboxOpen: invalid or missing user/token in request" client="%s" user="%s"' % (req.remote_addr, userid)) @@ -573,7 +574,8 @@ def cboxOpen_deprecated(): folderurl = url_unquote(req.args.get('folderurl', '%2F')) # defaults to `/` endpoint = req.args.get('endpoint', 'default') try: - inode, acctok = utils.generateAccessToken(userid, filename, viewmode, username, folderurl, endpoint, '', '', '') + # here we leave wopiuser empty as `userid` (i.e. uid:gid) is known to be consistent over time + inode, acctok = utils.generateAccessToken(userid, filename, viewmode, (username, ''), folderurl, endpoint, ('', '', '')) except IOError as e: Wopi.log.warning('msg="cboxOpen: remote error on generating token" client="%s" user="%s" ' \ 'friendlyname="%s" mode="%s" endpoint="%s" reason="%s"' %