diff --git a/invenio/modules/oauth2server/models.py b/invenio/modules/oauth2server/models.py index cb580145e..872e40dfb 100644 --- a/invenio/modules/oauth2server/models.py +++ b/invenio/modules/oauth2server/models.py @@ -1,376 +1,373 @@ # -*- coding: utf-8 -*- ## ## This file is part of Invenio. ## Copyright (C) 2014 CERN. ## ## Invenio is free software; you can redistribute it and/or ## modify it under the terms of the GNU General Public License as ## published by the Free Software Foundation; either version 2 of the ## License, or (at your option) any later version. ## ## Invenio is distributed in the hope that it will be useful, but ## WITHOUT ANY WARRANTY; without even the implied warranty of ## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU ## General Public License for more details. ## ## You should have received a copy of the GNU General Public License ## along with Invenio; if not, write to the Free Software Foundation, Inc., ## 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA. """Database models for OAuth2 server.""" from __future__ import absolute_import import six from flask import current_app from flask.ext.login import current_user from sqlalchemy_utils import URLType from sqlalchemy_utils.types.encrypted import EncryptedType, AesEngine from werkzeug.security import gen_salt from wtforms import validators from invenio.config import SECRET_KEY as secret_key from invenio.base.i18n import _ from invenio.ext.sqlalchemy import db from invenio.ext.login.legacy_user import UserInfo from .validators import validate_redirect_uri, validate_scopes from .errors import ScopeDoesNotExists class NoneAesEngine(AesEngine): """Filter None values from encrypting.""" - def __init__(self, key): - super(NoneAesEngine, self).__init__(key) - def encrypt(self, value): """Encrypt a value on the way in.""" if value is not None: return super(NoneAesEngine, self).encrypt(value) def decrypt(self, value): """Decrypt value on the way out.""" if value is not None: return super(NoneAesEngine, self).decrypt(value) class String255EncryptedType(EncryptedType): impl = db.String(255) class OAuthUserProxy(object): """Proxy object to an Invenio User.""" def __init__(self, user): """Initialize proxy object with user instance.""" self._user = user def __getattr__(self, name): """Pass any undefined attribute to the underlying object.""" return getattr(self._user, name) def __getstate__(self): return self.id def __setstate__(self, state): self._user = UserInfo(state) @property def id(self): """Return user identifier.""" return self._user.get_id() def check_password(self, password): """Check user password.""" return self.password == password @classmethod def get_current_user(cls): """Return an instance of current user object.""" return cls(current_user._get_current_object()) class Scope(object): """OAuth scope definition.""" def __init__(self, id_, help_text='', group='', internal=False): """Initialize scope values.""" self.id = id_ self.group = group self.help_text = help_text self.is_internal = internal class Client(db.Model): """A client is the app which want to use the resource of a user. It is suggested that the client is registered by a user on your site, but it is not required. The client should contain at least these information: client_id: A random string client_secret: A random string client_type: A string represents if it is confidential redirect_uris: A list of redirect uris default_redirect_uri: One of the redirect uris default_scopes: Default scopes of the client But it could be better, if you implemented: allowed_grant_types: A list of grant types allowed_response_types: A list of response types validate_scopes: A function to validate scopes """ __tablename__ = 'oauth2CLIENT' name = db.Column( db.String(40), info=dict( label=_('Name'), description=_('Name of application (displayed to users).'), validators=[validators.Required()] ) ) """Human readable name of the application.""" description = db.Column( db.Text(), default=u'', info=dict( label=_('Description'), description=_('Optional. Description of the application' ' (displayed to users).'), ) ) """Human readable description.""" website = db.Column( URLType(), info=dict( label=_('Website URL'), description=_('URL of your application (displayed to users).'), ), default=u'', ) user_id = db.Column(db.ForeignKey('user.id')) """Creator of the client application.""" client_id = db.Column(db.String(255), primary_key=True) """Client application ID.""" client_secret = db.Column( db.String(255), unique=True, index=True, nullable=False ) """Client application secret.""" is_confidential = db.Column(db.Boolean, default=True) """Determine if client application is public or not.""" is_internal = db.Column(db.Boolean, default=False) """Determins if client application is an internal application.""" _redirect_uris = db.Column(db.Text) """A newline-separated list of redirect URIs. First is the default URI.""" _default_scopes = db.Column(db.Text) """A space-separated list of default scopes of the client. The value of the scope parameter is expressed as a list of space-delimited, case-sensitive strings. """ user = db.relationship('User') """Relationship to user.""" @property def allowed_grant_types(self): return current_app.config['OAUTH2_ALLOWED_GRANT_TYPES'] @property def allowed_response_types(self): return current_app.config['OAUTH2_ALLOWED_RESPONSE_TYPES'] # def validate_scopes(self, scopes): # return self._validate_scopes @property def client_type(self): if self.is_confidential: return 'confidential' return 'public' @property def redirect_uris(self): if self._redirect_uris: return self._redirect_uris.splitlines() return [] @redirect_uris.setter def redirect_uris(self, value): """Validate and store redirect URIs for client.""" if isinstance(value, six.text_type): value = value.split("\n") value = [v.strip() for v in value] for v in value: validate_redirect_uri(v) self._redirect_uris = "\n".join(value) or "" @property def default_redirect_uri(self): try: return self.redirect_uris[0] except IndexError: pass @property def default_scopes(self): """List of default scopes for client.""" if self._default_scopes: return self._default_scopes.split(" ") return [] @default_scopes.setter def default_scopes(self, scopes): """Set default scopes for client.""" validate_scopes(scopes) self._default_scopes = " ".join(set(scopes)) if scopes else "" def validate_scopes(self, scopes): """Validate if client is allowed to access scopes.""" try: validate_scopes(scopes) return True except ScopeDoesNotExists: return False def gen_salt(self): self.reset_client_id() self.reset_client_secret() def reset_client_id(self): self.client_id = gen_salt( current_app.config.get('OAUTH2_CLIENT_ID_SALT_LEN') ) def reset_client_secret(self): self.client_secret = gen_salt( current_app.config.get('OAUTH2_CLIENT_SECRET_SALT_LEN') ) class Token(db.Model): """A bearer token is the final token that can be used by the client.""" __tablename__ = 'oauth2TOKEN' id = db.Column(db.Integer, primary_key=True, autoincrement=True) """Object ID.""" client_id = db.Column( db.String(40), db.ForeignKey('oauth2CLIENT.client_id'), nullable=False, ) """Foreign key to client application.""" client = db.relationship('Client') """SQLAlchemy relationship to client application.""" user_id = db.Column( db.Integer, db.ForeignKey('user.id') ) """Foreign key to user.""" user = db.relationship('User') """SQLAlchemy relationship to user.""" token_type = db.Column(db.String(255), default='bearer') """Token type - only bearer is supported at the moment.""" access_token = db.Column(String255EncryptedType( - type_in=lambda: db.String(255), + type_in=db.String(255), key=secret_key), unique=True ) refresh_token = db.Column(String255EncryptedType( - type_in=lambda: db.String(255), + type_in=db.String(255), key=secret_key, engine=NoneAesEngine), unique=True, nullable=True ) expires = db.Column(db.DateTime, nullable=True) _scopes = db.Column(db.Text) is_personal = db.Column(db.Boolean, default=False) """Personal accesss token.""" is_internal = db.Column(db.Boolean, default=False) """Determines if token is an internally generated token.""" @property def scopes(self): if self._scopes: return self._scopes.split() return [] @scopes.setter def scopes(self, scopes): validate_scopes(scopes) self._scopes = " ".join(set(scopes)) if scopes else "" def get_visible_scopes(self): """Get list of non-internal scopes for token.""" from .registry import scopes as scopes_registry return [k for k, s in scopes_registry.choices() if k in self.scopes] @classmethod def create_personal(cls, name, user_id, scopes=None, is_internal=False): """Create a personal access token. A token that is bound to a specific user and which doesn't expire, i.e. similar to the concept of an API key. """ scopes = " ".join(scopes) if scopes else "" c = Client( name=name, user_id=user_id, is_internal=True, is_confidential=False, _default_scopes=scopes ) c.gen_salt() t = Token( client_id=c.client_id, user_id=user_id, access_token=gen_salt( current_app.config.get('OAUTH2_TOKEN_PERSONAL_SALT_LEN') ), expires=None, _scopes=scopes, is_personal=True, is_internal=is_internal, ) db.session.add(c) db.session.add(t) db.session.commit() return t diff --git a/setup.py b/setup.py index 7d327d445..80e8ea7f6 100644 --- a/setup.py +++ b/setup.py @@ -1,302 +1,302 @@ # -*- coding: utf-8 -*- ## This file is part of Invenio. ## Copyright (C) 2013, 2014 CERN. ## ## Invenio is free software; you can redistribute it and/or ## modify it under the terms of the GNU General Public License as ## published by the Free Software Foundation; either version 2 of the ## License, or (at your option) any later version. ## ## Invenio is distributed in the hope that it will be useful, but ## WITHOUT ANY WARRANTY; without even the implied warranty of ## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU ## General Public License for more details. ## ## You should have received a copy of the GNU General Public License ## along with Invenio; if not, write to the Free Software Foundation, Inc., ## 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA. """ Invenio is Fun. Links ----- * `website <http://invenio-software.org/>`_ * `documentation <http://invenio.readthedocs.org/en/latest/>`_ * `development version <https://github.com/inveniosoftware/invenio>`_ """ import os import sys from setuptools import setup, find_packages from setuptools.command.install_lib import install_lib from distutils.command.build import build class _build(build): """Compile catalog before building the package.""" sub_commands = [('compile_catalog', None)] + build.sub_commands class _install_lib(install_lib): """Custom install_lib command.""" def run(self): """Compile catalog before running installation command.""" self.run_command('compile_catalog') install_lib.run(self) install_requires = [ "alembic>=0.6.6", "Babel>=1.3", "bagit>=1.5.1", "BeautifulSoup>=3.2.1", "BeautifulSoup4>=4.3.2", "celery>=3.1.8", # Cerberus>=0.7.1 api changes and is not yet supported "Cerberus>=0.7,<0.7.1", "chardet>=2.3.0", "dictdiffer>=0.0.3", "feedparser>=5.1", "fixture>=1.5", "Flask>=0.10.1", "Flask-Admin>=1.0.9", "Flask-Assets>=0.10", "Flask-Babel>=0.9", "Flask-Breadcrumbs>=0.2", "Flask-Cache>=0.12", "Flask-Collect>=1.1.1", "Flask-Email>=1.4.4", "Flask-Gravatar>=0.4", "Flask-Login>=0.2.7", "Flask-Menu>=0.2", "Flask-OAuthlib>=0.6.0,<0.7", # quick fix for issue #2158 "Flask-Principal>=0.4", "Flask-Registry>=0.2", "Flask-RESTful>=0.2.12", "Flask-Script>=2.0.5", # Development version is used, will switch to >=2.0 once released. "Flask-SQLAlchemy>=2.0", "Flask-WTF>=0.10.2", "fs>=0.4", "intbitset>=2.0", "jellyfish>=0.3.2", "Jinja2>=2.7", "libmagic>=1.0", "lxml>=3.3", "mechanize>=0.2.5", "mistune>=0.4.1", "msgpack-python>=0.3", "MySQL-python>=1.2.5", "numpy>=1.7", "nydus>=0.10.8", # pyparsing>=2.0.2 has a new api and is not compatible yet "pyparsing>=2.0.1,<2.0.2", "python-twitter>=0.8.7", "pyPDF>=1.13", "pyPDF2", "PyLD>=0.5.2", "pyStemmer>=1.3", "python-dateutil>=1.5", "python-magic>=0.4.6", "pytz", "rauth", "raven>=5.0.0", "rdflib>=4.1.2", "redis>=2.8.0", "reportlab>=2.7,<3.2", "requests>=2.3,<2.4", "setuptools>=2.2", "six>=1.7.2", "Sphinx", "SQLAlchemy>=0.9.8", - "SQLAlchemy-Utils[encrypted]>=0.27", + "SQLAlchemy-Utils[encrypted]>=0.28.2", "unidecode", "workflow>=1.2.0", "WTForms>=2.0.1", "wtforms-alchemy>=0.12.6" ] extras_require = { "docs": [ "sphinx_rtd_theme" ], "development": [ "Flask-DebugToolbar==0.9.0", ], "elasticsearch": [ "pyelasticsearch>=0.6.1" ], "img": [ "qrcode", "Pillow" ], "mongo": [ "pymongo" ], "misc": [ # was requirements-extras "apiclient", # extra=cloud? "dropbox", # extra=cloud? "gnuplot-py==1.8", "flake8", # extra=kwalitee? "pep8", # extra=kwalitee? "pychecker==0.8.19", # extra=kwalitee? "pylint", # extra=kwalitee? "nosexcover", # test? "oauth2client", # extra=cloud? "python-onedrive", # extra=cloud? "python-openid", # extra=sso? "urllib3", # extra=cloud? ], "mixer": [ "mixer", ], "sso": [ "Flask-SSO>=0.1" ], "postgresql": [ "psycopg2>=2.5", ], # Alternative XML parser # # For pyRXP, the version on PyPI many not be the right one. # # $ pip install # > https://www.reportlab.com/ftp/pyRXP-1.16-daily-unix.tar.gz#egg=pyRXP # "pyrxp": [ # Any other versions are not supported. "pyRXP==1.16-daily-unix" ], "github": [ "github3.py>=0.9" ], } extras_require["docs"] += extras_require["elasticsearch"] extras_require["docs"] += extras_require["img"] extras_require["docs"] += extras_require["mongo"] extras_require["docs"] += extras_require["sso"] extras_require["docs"] += extras_require["github"] tests_require = [ "httpretty>=0.8", "Flask-Testing>=0.4.1", "mock", "nose", "selenium", "unittest2>=0.5", ] # Compatibility with Python 2.6 if sys.version_info < (2, 7): install_requires += [ "argparse", "importlib" ] # Get the version string. Cannot be done with import! g = {} with open(os.path.join("invenio", "version.py"), "rt") as fp: exec(fp.read(), g) version = g["__version__"] packages = find_packages(exclude=['docs']) packages.append('invenio_docs') setup( name='Invenio', version=version, url='https://github.com/inveniosoftware/invenio', license='GPLv2', author='CERN', author_email='info@invenio-software.org', description='Digital library software', long_description=__doc__, packages=packages, package_dir={'invenio_docs': 'docs'}, include_package_data=True, zip_safe=False, platforms='any', entry_points={ 'console_scripts': [ 'inveniomanage = invenio.base.manage:main', 'plotextractor = invenio.utils.scripts.plotextractor:main', # Legacy 'alertengine = invenio.legacy.webalert.scripts.alertengine:main', 'batchuploader = invenio.legacy.bibupload.scripts.batchuploader', 'bibcircd = invenio.legacy.bibcirculation.scripts.bibcircd:main', 'bibauthorid = invenio.legacy.bibauthorid.scripts.bibauthorid:main', 'bibclassify = invenio.modules.classifier.scripts.classifier:main', 'bibconvert = invenio.legacy.bibconvert.scripts.bibconvert:main', 'bibdocfile = invenio.legacy.bibdocfile.scripts.bibdocfile:main', 'bibedit = invenio.legacy.bibedit.scripts.bibedit:main', 'bibencode = invenio.modules.encoder.scripts.encoder:main', 'bibexport = invenio.legacy.bibexport.scripts.bibexport:main', 'bibindex = invenio.legacy.bibindex.scripts.bibindex:main', 'bibmatch = invenio.legacy.bibmatch.scripts.bibmatch:main', 'bibrank = invenio.legacy.bibrank.scripts.bibrank:main', 'bibrankgkb = invenio.legacy.bibrank.scripts.bibrankgkb:main', 'bibreformat = invenio.legacy.bibformat.scripts.bibreformat:main', 'bibsort = invenio.legacy.bibsort.scripts.bibsort:main', 'bibsched = invenio.legacy.bibsched.scripts.bibsched:main', 'bibstat = invenio.legacy.bibindex.scripts.bibstat:main', 'bibtaskex = invenio.legacy.bibsched.scripts.bibtaskex:main', 'bibtasklet = invenio.legacy.bibsched.scripts.bibtasklet:main', 'bibupload = invenio.legacy.bibupload.scripts.bibupload:main', 'dbexec = invenio.legacy.miscutil.scripts.dbexec:main', 'dbdump = invenio.legacy.miscutil.scripts.dbdump:main', 'docextract = invenio.legacy.docextract.scripts.docextract:main', 'elmsubmit = invenio.legacy.elmsubmit.scripts.elmsubmit:main', 'gotoadmin = invenio.modules.redirector.scripts.redirector:main', 'inveniocfg = invenio.legacy.inveniocfg:main', 'inveniogc = invenio.legacy.websession.scripts.inveniogc:main', 'inveniounoconv = invenio.legacy.websubmit.scripts.inveniounoconv:main', 'oaiharvest = invenio.legacy.oaiharvest.scripts.oaiharvest:main', 'oairepositoryupdater = invenio.legacy.oairepository.scripts.oairepositoryupdater:main', 'arxiv-pdf-checker = invenio.legacy.pdfchecker:main', 'refextract = invenio.legacy.refextract.scripts.refextract:main', 'textmarc2xmlmarc = invenio.legacy.bibrecord.scripts.textmarc2xmlmarc:main', 'webaccessadmin = invenio.modules.access.scripts.webaccessadmin:main', 'webauthorprofile = invenio.legacy.webauthorprofile.scripts.webauthorprofile:main', 'webcoll = invenio.legacy.websearch.scripts.webcoll:main', 'webmessageadmin = invenio.legacy.webmessage.scripts.webmessageadmin:main', 'webstatadmin = invenio.legacy.webstat.scripts.webstatadmin:main', 'websubmitadmin = invenio.legacy.websubmit.scripts.websubmitadmin:main', 'xmlmarc2textmarc = invenio.legacy.bibrecord.scripts.xmlmarc2textmarc:main', 'xmlmarclint = invenio.legacy.bibrecord.scripts.xmlmarclint:main', ], "distutils.commands": [ "inveniomanage = invenio.base.setuptools:InvenioManageCommand", ] }, install_requires=install_requires, extras_require=extras_require, classifiers=[ 'Development Status :: 4 - Beta', 'Environment :: Web Environment', 'Intended Audience :: Developers', 'License :: OSI Approved :: GPLv2 License', 'Operating System :: OS Independent', 'Programming Language :: Python', 'Programming Language :: Python :: 2', 'Programming Language :: Python :: 2.6', 'Programming Language :: Python :: 2.7', 'Topic :: Internet :: WWW/HTTP :: Dynamic Content', ], test_suite='invenio.testsuite.suite', tests_require=tests_require, cmdclass={ 'build': _build, 'install_lib': _install_lib, }, )