def getUser(self, token): headers = { 'Authorization': 'Bearer %s' % token['access_token'], 'Accept': 'application/json' } # Get user's OAuth2 ID, email, and name # For privacy and efficiency, fetch only the specific needed fields # https://developer.linkedin.com/docs/signin-with-linkedin url = '%s:(%s)?%s' % (self._API_USER_URL, ','.join( self._API_USER_FIELDS), urllib.parse.urlencode({'format': 'json'})) resp = self._getJson(method='GET', url=url, headers=headers) oauthId = resp.get('id') if not oauthId: raise RestException('LinkedIn did not return user ID.', code=502) email = resp.get('emailAddress') if not email: raise RestException( 'This LinkedIn user has no registered email address.', code=502) # Get user's name firstName = resp.get('firstName', '') lastName = resp.get('lastName', '') user = self._createOrReuseUser(oauthId, email, firstName, lastName) return user
def getPagingParameters(self, params, defaultSortField=None, defaultSortDir=SortDir.ASCENDING): """ Pass the URL parameters into this function if the request is for a list of resources that should be paginated. It will return a tuple of the form (limit, offset, sort) whose values should be passed directly into the model methods that are finding the resources. If the client did not pass the parameters, this always uses the same defaults of limit=20000, offset=0, sort='name', sortdir=SortDir.ASCENDING=1. :param params: The URL query parameters. :type params: dict :param defaultSortField: If the client did not pass a 'sort' parameter, set this to choose a default sort field. If None, the results will be returned unsorted. :type defaultSortField: str or None :param defaultSortDir: Sort direction. :type defaultSortDir: girderformindlogger.constants.SortDir """ try: offset = int(params.get('offset', 0)) limit = int(params.get('limit', 50)) sortdir = int(params.get('sortdir', defaultSortDir)) except ValueError: raise RestException('Invalid value for offset, limit, or sortdir parameter.') if sortdir not in [SortDir.ASCENDING, SortDir.DESCENDING]: raise RestException('Invalid value for sortdir parameter.') if 'sort' in params: sort = [(params['sort'].strip(), sortdir)] elif isinstance(defaultSortField, six.string_types): sort = [(defaultSortField, sortdir)] else: sort = None return limit, offset, sort
def _validateCsrfToken(self, state): """ Tests the CSRF token value in the cookie to authenticate the user as the originator of the OAuth2 login. Raises a RestException if the token is invalid. """ csrfTokenId, _, redirect = state.partition('.') token = Token().load(csrfTokenId, objectId=False, level=AccessType.READ) if token is None: raise RestException('Invalid CSRF token (state="%s").' % state, code=403) Token().remove(token) if token['expires'] < datetime.datetime.utcnow(): raise RestException('Expired CSRF token (state="%s").' % state, code=403) if not redirect: raise RestException('No redirect location (state="%s").' % state) return redirect
def requireParams(self, required, provided=None): """ This method has two modes. In the first mode, this takes two parameters, the first being a required parameter or list of them, and the second the dictionary of parameters that were passed. If the required parameter does not appear in the passed parameters, a ValidationException is raised. The second mode of operation takes only a single parameter, which is a dict mapping required parameter names to passed in values for those params. If the value is ``None``, a ValidationException is raised. This mode works well in conjunction with the ``autoDescribeRoute`` decorator, where the parameters are not all contained in a single dictionary. :param required: An iterable of required params, or if just one is required, you can simply pass it as a string. :type required: `list, tuple, or str` :param provided: The list of provided parameters. :type provided: dict """ if provided is None and isinstance(required, dict): for name, val in six.viewitems(required): if val is None: raise RestException('Parameter "%s" is required.' % name) else: if isinstance(required, six.string_types): required = (required,) for param in required: if provided is None or param not in provided: raise RestException('Parameter "%s" is required.' % param)
def login(self): import threading from girderformindlogger.utility.mail_utils import validateEmailAddress if not Setting().get(SettingKey.ENABLE_PASSWORD_LOGIN): raise RestException('Password login is disabled on this instance.') user, token = self.getCurrentUser(returnToken=True) # Only create and send new cookie if user isn't already sending a valid # one. if not user: authHeader = cherrypy.request.headers.get('Authorization') if not authHeader: authHeader = cherrypy.request.headers.get( 'Girder-Authorization') if not authHeader or not authHeader[0:6] == 'Basic ': raise RestException('Use HTTP Basic Authentication', 401) try: credentials = base64.b64decode(authHeader[6:]).decode('utf8') if ':' not in credentials: raise TypeError except Exception: raise RestException('Invalid HTTP Authorization header', 401) login, password = credentials.split(':', 1) if validateEmailAddress(login): raise AccessException( "Please log in with a username, not an email address.") otpToken = cherrypy.request.headers.get('Girder-OTP') try: user = self._model.authenticate(login, password, otpToken) except: raise AccessException( "Incorrect password for {} if that user exists".format( login)) thread = threading.Thread( target=AppletModel().updateUserCacheAllRoles, args=(user, )) setCurrentUser(user) token = self.sendAuthTokenCookie(user) return { 'user': self._model.filter(user, user), 'authToken': { 'token': token['_id'], 'expires': token['expires'], 'scope': token['scope'] }, 'message': 'Login succeeded.' }
def _validateJsonType(self, name, info, val): if info.get('schema') is not None: try: jsonschema.validate(val, info['schema']) except jsonschema.ValidationError as e: raise RestException( 'Invalid JSON object for parameter %s: %s' % (name, str(e))) elif info['requireObject'] and not isinstance(val, dict): raise RestException('Parameter %s must be a JSON object.' % name) elif info['requireArray'] and not isinstance(val, list): raise RestException('Parameter %s must be a JSON array.' % name)
def setTimestamp(self, id, type, created, updated): user = self.getCurrentUser() model = self._getResourceModel(type) doc = model.load(id=id, user=user, level=AccessType.WRITE, exc=True) if created is not None: if 'created' not in doc: raise RestException('Resource has no "created" field.') doc['created'] = parseTimestamp(created) if updated is not None: if 'updated' not in doc: raise RestException('Resource has no "updated" field.') doc['updated'] = parseTimestamp(updated) return model.filter(model.save(doc), user=user)
def finalizeOtp(self, user): otpToken = cherrypy.request.headers.get('Girder-OTP') if not otpToken: raise RestException('The "Girder-OTP" header must be provided.') if 'otp' not in user: raise RestException('The user has not initialized one-time passwords.') if self._model.hasOtpEnabled(user): raise RestException('The user has already enabled one-time passwords.') user['otp']['enabled'] = True # This will raise an exception if the verification fails, so the user will not be saved self._model.verifyOtp(user, otpToken) self._model.save(user)
def updateProfile(self, update={}, id=None, applet=None, idCode=None): if (id is not None) and (applet is not None or idCode is not None): raise RestException( 'Pass __either__ profile ID __OR__ (applet ID and ID code), ' 'not both.') elif (id is None) and (applet is None or idCode is None): raise RestException( 'Either profile ID __OR__ (applet ID and ID code) required.') else: currentUser = self.getCurrentUser() id = id if id is not None else Profile().getProfile( applet=AppletModel().load(applet, force=True), idCode=idCode, user=currentUser) return (ProfileModel().updateProfile(id, currentUser, update))
def wrapped(*args, **kwargs): model = ModelImporter.model(self.model, self.plugin) for raw, converted in six.viewitems(self.map): id = self._getIdValue(kwargs, raw) if self.force: kwargs[converted] = model.load( id, force=True, **self.kwargs) elif self.level is not None: kwargs[converted] = model.load( id=id, level=self.level, user=getCurrentUser(), **self.kwargs) else: kwargs[converted] = model.load(id, **self.kwargs) if kwargs[converted] is None and self.exc: raise RestException( 'Invalid %s id (%s).' % (model.name, str(id))) if self.requiredFlags: model.requireAccessFlags( kwargs[converted], user=getCurrentUser(), flags=self.requiredFlags) return fun(*args, **kwargs)
def _getIdValue(self, kwargs, idParam): if idParam in kwargs: return kwargs.pop(idParam) elif idParam in kwargs['params']: return kwargs['params'].pop(idParam) else: raise RestException('No ID parameter passed: ' + idParam)
def _validateParam(self, name, descParam, value): """ Validates and transforms a single parameter that was passed. Raises RestException if the passed value is invalid. :param name: The name of the param. :type name: str :param descParam: The formal parameter in the Description. :type descParam: dict :param value: The value passed in for this param for the current request. :returns: The value transformed """ type = descParam.get('type') # Coerce to the correct data type if type == 'string': value = self._handleString(name, descParam, value) elif type == 'boolean': value = toBool(value) elif type == 'integer': value = self._handleInt(name, descParam, value) elif type == 'number': value = self._handleNumber(name, descParam, value) # Enum validation (should be after type coercion) if 'enum' in descParam and value not in descParam['enum']: raise RestException( 'Invalid value for %s: "%s". Allowed values: %s.' % (name, value, ', '.join(str(v) for v in descParam['enum']))) return value
def createAssetstore(self, name, type, root, perms, db, mongohost, replicaset, bucket, prefix, accessKeyId, secret, service, readOnly, region, inferCredentials, serverSideEncryption): if type == AssetstoreType.FILESYSTEM: self.requireParams({'root': root}) return self._model.createFilesystemAssetstore(name=name, root=root, perms=perms) elif type == AssetstoreType.GRIDFS: self.requireParams({'db': db}) return self._model.createGridFsAssetstore(name=name, db=db, mongohost=mongohost, replicaset=replicaset) elif type == AssetstoreType.S3: self.requireParams({'bucket': bucket}) return self._model.createS3Assetstore( name=name, bucket=bucket, prefix=prefix, secret=secret, accessKeyId=accessKeyId, service=service, readOnly=readOnly, region=region, inferCredentials=inferCredentials, serverSideEncryption=serverSideEncryption) else: raise RestException('Invalid type parameter')
def listResources(self, params): # Paths Object paths = {} # Definitions Object definitions = dict(**docs.models[None]) # List of Tag Objects tags = [] routeMap = _apiRouteMap() for resource in sorted(six.viewkeys(docs.routes), key=str): # Update Definitions Object if resource in docs.models: for name, model in six.viewitems(docs.models[resource]): definitions[name] = model prefixPath = None tag = resource if isinstance(resource, Resource): if resource not in routeMap: raise RestException('Resource not mounted: %s' % resource) prefixPath = routeMap[resource] tag = prefixPath[0] # Tag Object tags.append({'name': tag}) for route, methods in six.viewitems(docs.routes[resource]): # Path Item Object pathItem = {} for method, operation in six.viewitems(methods): # Operation Object pathItem[method.lower()] = operation if prefixPath: operation['tags'] = prefixPath[:1] if prefixPath: route = '/'.join([''] + prefixPath + [route[1:]]) paths[route] = pathItem apiUrl = getApiUrl(preferReferer=True) urlParts = getUrlParts(apiUrl) host = urlParts.netloc basePath = urlParts.path return { 'swagger': SWAGGER_VERSION, 'info': { 'title': 'Girder REST API', 'version': VERSION['release'] }, 'host': host, 'basePath': basePath, 'tags': tags, 'paths': paths, 'definitions': definitions }
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 wrapped(*args, **kwargs): """ Transform any passed params according to the spec, or fill in default values for any params not passed. """ # Combine path params with form/query params into a single lookup table params = {k: v for k, v in six.viewitems(kwargs) if k != 'params'} params.update(kwargs.get('params', {})) kwargs['params'] = kwargs.get('params', {}) for descParam in self.description.params: # We need either a type or a schema ( for message body ) if 'type' not in descParam and 'schema' not in descParam: continue name = descParam['name'] model = self._getModel(name, self.description.modelParams) if name in params: if name in self.description.jsonParams: info = self.description.jsonParams[name] val = self._loadJson(name, info, params[name]) self._passArg(fun, kwargs, name, val) elif name in self.description.modelParams: info = self.description.modelParams[name] kwargs.pop(name, None) # Remove from path params val = self._loadModel(name, info, params[name], model) self._passArg(fun, kwargs, self._destName(info, model), val) else: val = self._validateParam(name, descParam, params[name]) self._passArg(fun, kwargs, name, val) elif descParam['in'] == 'body': if name in self.description.jsonParams: info = self.description.jsonParams[name].copy() info['required'] = descParam['required'] val = self._loadJsonBody(name, info) self._passArg(fun, kwargs, name, val) else: self._passArg(fun, kwargs, name, cherrypy.request.body) elif descParam['in'] == 'header': continue # For now, do nothing with header params elif 'default' in descParam: self._passArg(fun, kwargs, name, descParam['default']) elif descParam['required']: raise RestException('Parameter "%s" is required.' % name) else: # If required=False but no default is specified, use None if name in self.description.modelParams: info = self.description.modelParams[name] kwargs.pop(name, None) # Remove from path params self._passArg(fun, kwargs, info['destName'] or model.name, None) else: self._passArg(fun, kwargs, name, None) self._mungeKwargs(kwargs, fun) return fun(*args, **kwargs)
def _matchRoute(self, method, path): """ Helper function that attempts to match the requested ``method`` and ``path`` with a registered route specification. :param method: The requested HTTP method, in lowercase. :type method: str :param path: The requested path. :type path: tuple[str] :returns: A tuple of ``(route, handler, wildcards)``, where ``route`` is the registered `list` of route components, ``handler`` is the route handler `function`, and ``wildcards`` is a `dict` of kwargs that should be passed to the underlying handler, based on the wildcard tokens of the route. :raises: `GirderException`, when no routes are defined on this resource. :raises: `RestException`, when no route can be matched. """ if not self._routes: raise GirderException('No routes defined for resource') for route, handler in self._routes[method][len(path)]: wildcards = {} for routeComponent, pathComponent in six.moves.zip(route, path): if routeComponent[0] == ':': # Wildcard token wildcards[routeComponent[1:]] = pathComponent elif routeComponent != pathComponent: # Exact match token break else: return route, handler, wildcards raise RestException('No matching route for "%s %s"' % (method.upper(), '/'.join(path)))
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 find(self, parentType, parentId, text, name, limit, offset, sort): """ Get a list of folders with given search parameters. Currently accepted search modes are: 1. Searching by parentId and parentType, with optional additional filtering by the name field (exact match) or using full text search within a single parent folder. Pass a "name" parameter or "text" parameter to invoke these additional filters. 2. Searching with full text search across all folders in the system. Simply pass a "text" parameter for this mode. """ user = self.getCurrentUser() if parentType and parentId: parent = ModelImporter.model(parentType).load( parentId, user=user, level=AccessType.READ, exc=True) filters = {} if text: filters['$text'] = { '$search': text } if name: filters['name'] = name return self._model.childFolders( parentType=parentType, parent=parent, user=user, offset=offset, limit=limit, sort=sort, filters=filters) elif text: return self._model.textSearch( text, user=user, limit=limit, offset=offset, sort=sort) else: raise RestException('Invalid search mode.')
def download(self, resources, includeMetadata): """ Returns a generator function that will be used to stream out a zip file containing the listed resource's contents, filtered by permissions. """ user = self.getCurrentUser() self._validateResourceSet(resources) # Check that all the resources are valid, so we don't download the zip # file if it would throw an error. for kind in resources: model = self._getResourceModel(kind, 'fileList') for id in resources[kind]: if not model.load(id=id, user=user, level=AccessType.READ): raise RestException('Resource %s %s not found.' % (kind, id)) setResponseHeader('Content-Type', 'application/zip') setContentDisposition('Resources.zip') def stream(): zip = ziputil.ZipGenerator() for kind in resources: model = ModelImporter.model(kind) for id in resources[kind]: doc = model.load(id=id, user=user, level=AccessType.READ) for (path, file) in model.fileList( doc=doc, user=user, includeMetadata=includeMetadata, subpath=True): for data in zip.addFile(file, path): yield data yield zip.footer() return stream
def download(self, item, offset, format, contentDisposition, extraParameters): user = self.getCurrentUser() files = list(self._model.childFiles(item=item, limit=2)) if format not in (None, '', 'zip'): raise RestException('Unsupported format: %s.' % format) if len(files) == 1 and format != 'zip': if contentDisposition not in {None, 'inline', 'attachment'}: raise RestException('Unallowed contentDisposition type "%s".' % contentDisposition) return File().download(files[0], offset, contentDisposition=contentDisposition, extraParameters=extraParameters) else: return self._downloadMultifileItem(item, user)
def initializeOtp(self, user): if self._model.hasOtpEnabled(user): raise RestException('The user has already enabled one-time passwords.') otpUris = self._model.initializeOtp(user) self._model.save(user) return otpUris
def sendVerificationEmail(self, email): user = self._model.findOne({'email': email}) if not user: raise RestException('That login is not registered.', 401) self._model._sendVerificationEmail(user, email) return {'message': 'Sent verification email.'}
def _validateAlgo(self, algo): """ Print an exception if a user requests an invalid checksum algorithm. """ if algo not in SUPPORTED_ALGORITHMS: msg = 'Invalid algorithm "%s". Supported algorithms: %s.' % ( algo, ', '.join(SUPPORTED_ALGORITHMS)) raise RestException(msg, code=400)
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 sendVerificationEmail(self, login): loginField = 'email' if '@' in login else 'login' user = self._model.findOne({loginField: login.lower()}) if not user: raise RestException('That login is not registered.', 401) self._model._sendVerificationEmail(user) return {'message': 'Sent verification email.'}
def _validateResourceSet(self, resources, allowedModels=None): """ Validate a set of resources against a set of allowed models. Also ensures the requested resource set is not empty. # TODO jsonschema could replace this probably :param resources: The set of resources requested. :param allowedModels: if present, an iterable of models that may be included in the resources. """ if allowedModels: invalid = set(resources.keys()) - set(allowedModels) if invalid: raise RestException('Invalid resource types requested: ' + ', '.join(invalid)) count = sum([len(v) for v in six.viewvalues(resources)]) if not count: raise RestException('No resources specified.')
def _loadJson(self, name, info, value): try: val = bson.json_util.loads(value) except ValueError: raise RestException('Parameter %s must be valid JSON.' % name) self._validateJsonType(name, info, val) return val
def _prepareMoveOrCopy(self, resources, parentType, parentId): user = self.getCurrentUser() self._validateResourceSet(resources, ('folder', 'item')) if resources.get('item') and parentType != 'folder': raise RestException('Invalid parentType.') return ModelImporter.model(parentType).load(parentId, level=AccessType.WRITE, user=user, exc=True)
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.')