def initializeOtp(self, user): """ Initialize the use of one-time passwords with this user. This does not save the modified user model. :param user: The user to modify. :return: The new OTP keys, each in KeyUriFormat. :rtype: dict """ totp = self._TotpFactory.new() user['otp'] = {'enabled': False, 'totp': totp.to_dict()} # Use the brand name as the OTP issuer if it's non-default (since that's prettier and more # meaningful for users), but fallback to the site hostname if the brand name isn't set # (to disambiguate otherwise identical "Girder" issuers) # Prevent circular import from girderformindlogger.api.rest import getUrlParts brandName = Setting().get(SettingKey.BRAND_NAME) defaultBrandName = Setting().getDefault(SettingKey.BRAND_NAME) # OTP URIs ( https://github.com/google/google-authenticator/wiki/Key-Uri-Format ) do not # allow colons, so use only the hostname component serverHostname = getUrlParts().netloc.partition(':')[0] # Normally, the issuer would be set when "self._TotpFactory" is instantiated, but that # happens during model initialization, when there's no current request, so the server # hostname is not known then otpIssuer = brandName if brandName != defaultBrandName else serverHostname return {'totpUri': totp.to_uri(label=user['login'], issuer=otpIssuer)}
def _setCommonCORSHeaders(): """ Set CORS headers that should be passed back with either a preflight OPTIONS or a simple CORS request. We set these headers anytime there is an Origin header present since browsers will simply ignore them if the request is not cross-origin. """ origin = cherrypy.request.headers.get('origin') if not origin: # If there is no origin header, this is not a cross origin request return allowed = Setting().get(SettingKey.CORS_ALLOW_ORIGIN) if allowed: setResponseHeader('Access-Control-Allow-Credentials', 'true') setResponseHeader( 'Access-Control-Expose-Headers', Setting().get(SettingKey.CORS_EXPOSE_HEADERS)) allowedList = {o.strip() for o in allowed.split(',')} if origin in allowedList: setResponseHeader('Access-Control-Allow-Origin', origin) elif '*' in allowedList: setResponseHeader('Access-Control-Allow-Origin', '*')
def hasCreatePrivilege(self, user): """ Tests whether a given user has the authority to create collections on this instance. This is based on the collection creation policy settings. By default, only admins are allowed to create collections. :param user: The user to test. :returns: bool """ from girderformindlogger.models.setting import Setting if user['admin']: return True policy = Setting().get(SettingKey.COLLECTION_CREATE_POLICY) if policy['open'] is True: return True if user['_id'] in policy.get('users', ()): return True if set(policy.get('groups', ())) & set(user.get('groups', ())): return True return False
def getCollectionCreationPolicyAccess(self): cpp = Setting().get(SettingKey.COLLECTION_CREATE_POLICY) acList = { 'users': [{ 'id': x } for x in cpp.get('users', [])], 'groups': [{ 'id': x } for x in cpp.get('groups', [])] } for user in acList['users'][:]: userDoc = User().load(user['id'], force=True, fields=['firstName', 'login', 'email']) if userDoc is None: acList['users'].remove(user) else: user['login'] = userDoc['login'] user['name'] = userDoc['firstName'] user['email'] = userDoc['email'] for grp in acList['groups'][:]: grpDoc = Group().load(grp['id'], force=True, fields=['name', 'description']) if grpDoc is None: acList['groups'].remove(grp) else: grp['name'] = grpDoc['name'] grp['description'] = grpDoc['description'] return acList
def testAutoComputeHashes(self): with self.assertRaises(ValidationException): Setting().set(hashsum_download.PluginSettings.AUTO_COMPUTE, 'bad') old = hashsum_download.SUPPORTED_ALGORITHMS hashsum_download.SUPPORTED_ALGORITHMS = {'sha512', 'sha256'} Setting().set(hashsum_download.PluginSettings.AUTO_COMPUTE, True) file = Upload().uploadFromFile( obj=six.BytesIO(self.userData), size=len(self.userData), name='Another file', parentType='folder', parent=self.privateFolder, user=self.user) start = time.time() while time.time() < start + 15: file = File().load(file['_id'], force=True) if 'sha256' in file: break time.sleep(0.2) expected = hashlib.sha256() expected.update(self.userData) self.assertIn('sha256', file) self.assertEqual(file['sha256'], expected.hexdigest()) expected = hashlib.sha512() expected.update(self.userData) self.assertIn('sha512', file) self.assertEqual(file['sha512'], expected.hexdigest()) hashsum_download.SUPPORTED_ALGORITHMS = old
def _setQuotaDefault(self, model, value, testVal='__NOCHECK__', error=None): """ Set the default quota for a particular model. :param model: either 'user' or 'collection'. :param value: the value to set. Either None or a positive integer. :param testVal: if not __NOCHECK__, test the current value to see if it matches this. :param error: if set, this is a substring expected in an error message. """ if model == 'user': key = PluginSettings.DEFAULT_USER_QUOTA elif model == 'collection': key = PluginSettings.DEFAULT_COLLECTION_QUOTA try: Setting().set(key, value) except ValidationException as err: if not error: raise if error not in err.args[0]: raise return if testVal != '__NOCHECK__': newVal = Setting().get(key) self.assertEqual(newVal, testVal)
def getLicenses(self, default): if default: licenses = Setting().getDefault(PluginSettings.LICENSES) else: licenses = Setting().get(PluginSettings.LICENSES) return licenses
def emailVerificationRequired(self, user): """ Returns True if email verification is required and this user has not yet verified their email address. """ from girderformindlogger.models.setting import Setting return (not user['emailVerified']) and \ (Setting().get(SettingKey.EMAIL_VERIFICATION) == 'required' or Setting().get(SettingKey.EMAIL_VERIFICATION) == 'enabled')
def OPTIONS(self, *path, **param): _setCommonCORSHeaders() cherrypy.lib.caching.expires(0) allowHeaders = Setting().get(SettingKey.CORS_ALLOW_HEADERS) allowMethods = Setting().get(SettingKey.CORS_ALLOW_METHODS) setResponseHeader('Access-Control-Allow-Methods', allowMethods) setResponseHeader('Access-Control-Allow-Headers', allowHeaders)
def testLicensesSettingValidation(self): """ Test validation of licenses setting. """ # Test valid settings Setting().set( PluginSettings.LICENSES, []) Setting().set( PluginSettings.LICENSES, [{'category': 'A', 'licenses': []}]) Setting().set( PluginSettings.LICENSES, [{'category': 'A', 'licenses': [{'name': '1'}]}]) Setting().set( PluginSettings.LICENSES, [{'category': 'A', 'licenses': [{'name': '1'}, {'name': '2'}]}]) Setting().set( PluginSettings.LICENSES, [{'category': 'A', 'licenses': []}, {'category': 'B', 'licenses': [{'name': '1'}]}]) Setting().set( PluginSettings.LICENSES, [{'category': 'A', 'licenses': []}, {'category': 'B', 'licenses': [{'name': '1'}, {'name': '2'}]}]) # Test invalid top-level types for val in (None, 1, '', {}, [{}]): self.assertRaises(ValidationException, Setting().set, PluginSettings.LICENSES, val) # Test invalid category types for category, licenses in ((None, []), (1, []), ('', []), ({}, [])): self.assertRaises( ValidationException, Setting().set, PluginSettings.LICENSES, [{'category': category, 'licenses': licenses}]) # Test invalid licenses types for val in (None, {}, [1], ['']): self.assertRaises( ValidationException, Setting().set, PluginSettings.LICENSES, [{'category': 'A', 'licenses': val}]) # Test invalid license names for val in (None, 1, '', {}, []): self.assertRaises( ValidationException, Setting().set, PluginSettings.LICENSES, [{'category': 'A', 'licenses': [{'name': val}]}])
def _submitEmail(msg, recipients): from girderformindlogger.models.setting import Setting setting = Setting() smtp = _SMTPConnection(host=setting.get(SettingKey.SMTP_HOST), port=setting.get(SettingKey.SMTP_PORT), encryption=setting.get(SettingKey.SMTP_ENCRYPTION), username=setting.get(SettingKey.SMTP_USERNAME), password=setting.get(SettingKey.SMTP_PASSWORD)) logger.info('Sending email to %s through %s', ', '.join(recipients), smtp.host) with smtp: smtp.send(msg['From'], recipients, msg.as_string())
def _createMessage(subject, text, to, bcc): from girderformindlogger.models.setting import Setting # Coerce and validate arguments if isinstance(to, six.string_types): to = [to] if isinstance(bcc, six.string_types): bcc = [bcc] elif bcc is None: bcc = [] if not to and not bcc: raise Exception('You must specify email recipients via "to" or "bcc".') if not subject: subject = '[no subject]' if isinstance(text, six.text_type): # TODO: needed? text = text.encode('utf8') # Build message msg = MIMEText(text, 'html', 'UTF-8') if to: msg['To'] = ', '.join(to) if bcc: msg['Bcc'] = ', '.join(bcc) msg['Subject'] = subject msg['From'] = Setting().get(SettingKey.EMAIL_FROM_ADDRESS) # Compute recipients recipients = list(set(to) | set(bcc)) return msg, recipients
def _ldapAuth(event): login, password = event.info['login'], event.info['password'] servers = Setting().get(PluginSettings.SERVERS) for server in servers: try: # ldap requires a uri complete with protocol. # Append one if the user did not specify. conn = ldap.initialize(server['uri']) conn.set_option(ldap.OPT_TIMEOUT, _CONNECT_TIMEOUT) conn.set_option(ldap.OPT_NETWORK_TIMEOUT, _CONNECT_TIMEOUT) conn.bind_s(server['bindName'], server['password'], ldap.AUTH_SIMPLE) searchStr = '%s=%s' % (server['searchField'], login) # Add the searchStr to the attributes, keep local scope. lattr = _LDAP_ATTRS + (server['searchField'],) results = conn.search_s(server['baseDn'], ldap.SCOPE_SUBTREE, searchStr, lattr) if results: entry, attrs = results[0] dn = attrs['distinguishedName'][0].decode('utf8') try: conn.bind_s(dn, password, ldap.AUTH_SIMPLE) except ldap.LDAPError: # Try other LDAP servers or fall back to core auth continue finally: conn.unbind_s() user = _getLdapUser(attrs, server) if user: event.stopPropagation().preventDefault().addResponse(user) except ldap.LDAPError: logger.exception('LDAP connection exception (%s).' % server['uri']) continue
def createToken(self, key, days=None): """ Create a token using an API key. :param key: The API key (the key itself, not the full document). :type key: str :param days: You may request a token duration up to the token duration of the API key itself, or pass None to use the API key duration. :type days: float or None """ from girderformindlogger.models.setting import Setting from girderformindlogger.models.token import Token from girderformindlogger.models.user import User apiKey = self.findOne({ 'key': key }) if apiKey is None or not apiKey['active']: raise ValidationException('Invalid API key.') cap = apiKey['tokenDuration'] or Setting().get(SettingKey.COOKIE_LIFETIME) days = min(float(days or cap), cap) user = User().load(apiKey['userId'], force=True) # Mark last used stamp apiKey['lastUse'] = datetime.datetime.utcnow() apiKey = self.save(apiKey) token = Token().createToken(user=user, days=days, scope=apiKey['scope'], apiKey=apiKey) return (user, token)
def generateTemporaryPassword(self, email): user = self._model.findOne({'email': self._model.hash(email.lower()), 'email_encrypted': True}) if not user: user = self._model.findOne({'email': email.lower(), 'email_encrypted': {'$ne': True}}) if not user: raise RestException('That email is not registered.') token = Token().createToken(user, days=(15/1440.0), scope=TokenScope.TEMPORARY_USER_AUTH) url = '%s#useraccount/%s/token/%s' % ( mail_utils.getEmailUrlPrefix(), str(user['_id']), str(token['_id'])) html = mail_utils.renderTemplate('temporaryAccess.mako', { 'url': url, 'token': str(token['_id']) }) mail_utils.sendMail( '%s: Temporary access' % Setting().get(SettingKey.BRAND_NAME), html, [email] ) return {'message': 'Sent temporary access email.'}
def _addDefaultFolders(self, event): """ This callback creates "Public" and "Private" folders on a user, after it is first created. This generally should not be called or overridden directly, but it may be unregistered from the `model.user.save.created` event. """ from girderformindlogger.models.folder import Folder from girderformindlogger.models.setting import Setting if Setting().get(SettingKey.USER_DEFAULT_FOLDERS) == 'public_private': user = event.info publicFolder = Folder().createFolder(user, 'Public', parentType='user', public=True, creator=user) privateFolder = Folder().createFolder(user, 'Private', parentType='user', public=False, creator=user) # Give the user admin access to their own folders Folder().setUserAccess(publicFolder, user, AccessType.ADMIN, save=True) Folder().setUserAccess(privateFolder, user, AccessType.ADMIN, save=True)
def getApiUrl(url=None, preferReferer=False): """ In a request thread, call this to get the path to the root of the REST API. The returned path does *not* end in a forward slash. :param url: URL from which to extract the base URL. If not specified, uses the server root system setting. If that is not specified, uses `cherrypy.url()` :param preferReferer: if no url is specified, this is true, and this is in a cherrypy request that has a referer header that contains the api string, use that referer as the url. """ apiStr = config.getConfig()['server']['api_root'] if not url: if preferReferer and apiStr in cherrypy.request.headers.get('referer', ''): url = cherrypy.request.headers['referer'] else: root = Setting().get(SettingKey.SERVER_ROOT) if root: return posixpath.join(root, apiStr.lstrip('/')) url = url or cherrypy.url() idx = url.find(apiStr) if idx < 0: raise GirderException('Could not determine API root in %s.' % url) return url[:idx + len(apiStr)]
def _computeHashHook(event): """ Event hook that computes the file hashes in the background after a completed upload. Only done if the AUTO_COMPUTE setting enabled. """ if Setting().get(PluginSettings.AUTO_COMPUTE): _computeHash(event.info['file'])
def stream(self, timeout, params): if not Setting().get(SettingKey.ENABLE_NOTIFICATION_STREAM): raise RestException('The notification stream is not enabled.', code=503) user, token = self.getCurrentUser(returnToken=True) setResponseHeader('Content-Type', 'text/event-stream') setResponseHeader('Cache-Control', 'no-cache') since = params.get('since') if since is not None: since = datetime.utcfromtimestamp(since) def streamGen(): lastUpdate = since start = time.time() wait = MIN_POLL_INTERVAL while cherrypy.engine.state == cherrypy.engine.states.STARTED: wait = min(wait + MIN_POLL_INTERVAL, MAX_POLL_INTERVAL) for event in NotificationModel().get(user, lastUpdate, token=token): if lastUpdate is None or event['updated'] > lastUpdate: lastUpdate = event['updated'] wait = MIN_POLL_INTERVAL start = time.time() yield sseMessage(event) if time.time() - start > timeout: break time.sleep(wait) return streamGen
def updateItemLicense(event): """ REST event handler to update item with license parameter, if provided. """ params = event.info['params'] if 'license' not in params: return itemModel = Item() item = itemModel.load(event.info['returnVal']['_id'], force=True, exc=True) newLicense = validateString(params['license']) if item['license'] == newLicense: return # Ensure that new license name is in configured list of licenses. # # Enforcing this here, instead of when validating the item, avoids an extra # database lookup (for the settings) on every future item save. if newLicense: licenseSetting = Setting().get(PluginSettings.LICENSES) validLicense = any( license['name'] == newLicense for group in licenseSetting for license in group['licenses']) if not validLicense: raise ValidationException( 'License name must be in configured list of licenses.', 'license') item['license'] = newLicense item = itemModel.save(item) event.preventDefault() event.addResponse(item)
def testManualComputeHashes(self): Setting().set(hashsum_download.PluginSettings.AUTO_COMPUTE, False) old = hashsum_download.SUPPORTED_ALGORITHMS hashsum_download.SUPPORTED_ALGORITHMS = {'sha512', 'sha256'} self.assertNotIn('sha256', self.privateFile) expected = hashlib.sha256() expected.update(self.userData) # Running the compute endpoint should only compute the missing ones resp = self.request( '/file/%s/hashsum' % self.privateFile['_id'], method='POST', user=self.user) self.assertStatusOk(resp) self.assertEqual(resp.json, { 'sha256': expected.hexdigest() }) # Running again should be a no-op resp = self.request( '/file/%s/hashsum' % self.privateFile['_id'], method='POST', user=self.user) self.assertStatusOk(resp) self.assertEqual(resp.json, None) file = File().load(self.privateFile['_id'], force=True) self.assertEqual(file['sha256'], expected.hexdigest()) hashsum_download.SUPPORTED_ALGORITHMS = old
def getEmailUrlPrefix(): """ Return the URL prefix for links back to the server. This is the link to the server root, so Girder-level path information and any query parameters or fragment value should be appended to this value. """ from girderformindlogger.models.setting import Setting return Setting().get(SettingKey.EMAIL_HOST)
def setUp(self): base.TestCase.setUp(self) self.siteAdminUser = User().createUser(email='*****@*****.**', login='******', firstName='Robert', lastName='Balboa', password='******') self.creatorUser = User().createUser(email='*****@*****.**', login='******', firstName='Apollo', lastName='Creed', password='******') creationSetting = Setting().getDefault( SettingKey.COLLECTION_CREATE_POLICY) creationSetting['open'] = True Setting().set(SettingKey.COLLECTION_CREATE_POLICY, creationSetting)
def adminApprovalRequired(self, user): """ Returns True if the registration policy requires admin approval and this user is pending approval. """ from girderformindlogger.models.setting import Setting return user.get('status', 'enabled') == 'pending' and \ Setting().get(SettingKey.REGISTRATION_POLICY) == 'approve'
def createUser( self, login, password, displayName="", email="", admin=False, lastName=None, firstName=None ): # 🔥 delete lastName once fully deprecated currentUser = self.getCurrentUser() regPolicy = Setting().get(SettingKey.REGISTRATION_POLICY) if not currentUser or not currentUser['admin']: admin = False if regPolicy == 'closed': raise RestException( 'Registration on this instance is closed. Contact an ' 'administrator to create an account for you.') user = self._model.createUser( login=login, password=password, email=email, firstName=displayName if len( displayName ) else firstName if firstName is not None else "", lastName=lastName, admin=admin, currentUser=currentUser) # 🔥 delete firstName and lastName once fully deprecated if not currentUser and self._model.canLogin(user): setCurrentUser(user) token = self.sendAuthTokenCookie(user) user['authToken'] = { 'token': token['_id'], 'expires': token['expires'] } # Assign all new users to a "New Users" Group newUserGroup = GroupModel().findOne({'name': 'New Users'}) newUserGroup = newUserGroup if ( newUserGroup is not None and bool(newUserGroup) ) else GroupModel( ).createGroup( name="New Users", creator=UserModel().findOne( query={'admin': True}, sort=[('created', SortDir.ASCENDING)] ), public=False ) group = GroupModel().addUser( newUserGroup, user, level=AccessType.READ ) group['access'] = GroupModel().getFullAccessList(group) group['requests'] = list(GroupModel().getFullRequestList(group)) return(user)
def testGetLicenses(self): """ Test getting list of licenses. """ # Get default settings resp = self.request(path='/item/licenses', user=self.user, params={ 'default': True }) self.assertStatusOk(resp) self.assertGreater(len(resp.json), 1) self.assertIn('category', resp.json[0]) self.assertIn('licenses', resp.json[0]) self.assertGreater(len(resp.json[0]['licenses']), 8) self.assertIn('name', resp.json[0]['licenses'][0]) self.assertGreater(len(resp.json[0]['licenses'][0]['name']), 0) self.assertIn('name', resp.json[0]['licenses'][1]) self.assertGreater(len(resp.json[0]['licenses'][1]['name']), 0) # Get current settings resp = self.request(path='/item/licenses', user=self.user) self.assertStatusOk(resp) self.assertGreater(len(resp.json), 1) self.assertIn('category', resp.json[0]) self.assertIn('licenses', resp.json[0]) self.assertGreater(len(resp.json[0]['licenses']), 8) self.assertIn('name', resp.json[0]['licenses'][0]) self.assertGreater(len(resp.json[0]['licenses'][0]['name']), 0) self.assertIn('name', resp.json[0]['licenses'][1]) self.assertGreater(len(resp.json[0]['licenses'][1]['name']), 0) # Change licenses Setting().set( PluginSettings.LICENSES, [{'category': 'A', 'licenses': [{'name': '1'}]}, {'category': 'B', 'licenses': [{'name': '2'}, {'name': '3'}]}]) # Get default settings after changing licenses resp = self.request(path='/item/licenses', user=self.user, params={ 'default': True }) self.assertStatusOk(resp) self.assertStatusOk(resp) self.assertGreater(len(resp.json), 1) self.assertIn('category', resp.json[0]) self.assertIn('licenses', resp.json[0]) self.assertGreater(len(resp.json[0]['licenses']), 8) self.assertIn('name', resp.json[0]['licenses'][0]) self.assertGreater(len(resp.json[0]['licenses'][0]['name']), 0) self.assertIn('name', resp.json[0]['licenses'][1]) self.assertGreater(len(resp.json[0]['licenses'][1]['name']), 0) # Get current settings after changing licenses resp = self.request(path='/item/licenses', user=self.user) self.assertStatusOk(resp) six.assertCountEqual( self, resp.json, [{'category': 'A', 'licenses': [{'name': '1'}]}, {'category': 'B', 'licenses': [{'name': '2'}, {'name': '3'}]}])
def setSetting(self, key, value, list): if list is None: list = ({'key': key, 'value': value}, ) for setting in list: key, value = setting['key'], setting['value'] if isinstance(value, six.string_types): try: value = json.loads(value) except ValueError: pass if value is None: Setting().unset(key=key) else: Setting().set(key=key, value=value) return True
def destroy(self, path): """ Handle shutdown of the FUSE. :param path: always '/'. """ Setting().unset(SettingKey.GIRDER_MOUNT_INFORMATION) events.trigger('server_fuse.destroy') return super(ServerFuse, self).destroy(path)
def computeBaseUrl(user): """ Compute the base gravatar URL for a user and return it. For the moment, the current default image is cached in this URL. It is the caller's responsibility to save this value on the user document. """ defaultImage = Setting().get(PluginSettings.DEFAULT_IMAGE) md5 = hashlib.md5(user['email'].encode('utf8')).hexdigest() return 'https://www.gravatar.com/avatar/%s?d=%s' % (md5, defaultImage)
def createKey(self, name, scope, tokenDuration, active): if Setting().get(SettingKey.API_KEYS): return ApiKeyModel().createApiKey(user=self.getCurrentUser(), name=name, scope=scope, days=tokenDuration, active=active) else: raise RestException( 'API key functionality is disabled on this instance.')