jeffknupp / sandman2

Automatically generate a RESTful API service for your legacy database. No code required!
Apache License 2.0
2k stars 215 forks source link

Add role based security or any kind of access control #114

Open supersexy opened 5 years ago

supersexy commented 5 years ago

Interesting project, but I can not find any hint about how to control access to data, also the documentation does not provide any concept regarding data security.

Some kind of access control system seems like a very basic requirement for any data access software - is this something that is planned for the future?

dkatz23238 commented 4 years ago

This would be a killer feature. Currently using nginx as a proxy.

Carelvd commented 4 years ago

If one cracks open the code one finds the following for the create application method

def get_app(
        database_uri,
        exclude_tables=None,
        user_models=None,
        reflect_all=True,
        read_only=False,
        schema=None):
    """..."""
    app = Flask('sandman2')
    app.config['SQLALCHEMY_DATABASE_URI'] = database_uri
    app.config['SANDMAN2_READ_ONLY'] = read_only
    app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
    app.classes = []
    db.init_app(app)
    admin = Admin(app, base_template='layout.html', template_mode='bootstrap3')
    _register_error_handlers(app)
    if user_models:
        with app.app_context():
            _register_user_models(user_models, admin, schema=schema)
    elif reflect_all:
        with app.app_context():
            _reflect_all(exclude_tables, admin, read_only, schema=schema)

    @app.route('/')
    def index():
        """Return a list of routes to the registered classes."""
        routes = {}
        for cls in app.classes:
            routes[cls.__model__.__name__] = '{}{{/{}}}'.format(
                cls.__model__.__url__,
                cls.__model__.primary_key())
        return jsonify(routes)
    return app

Which one can restructure as follows

def create_app(
        database_uri,
        exclude_tables=None,
        user_models=None,
        reflect_all=True,
        read_only=False,
        schema=None):
    app = Flask('sandman2')
    app.config['SQLALCHEMY_DATABASE_URI'] = database_uri
    app.config['SANDMAN2_READ_ONLY'] = read_only
    app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
    app.classes = []
    sandman(
        application,
        database_uri,
        exclude_tables=None,
        user_models=None,
        reflect_all=True,
        read_only=False,
        schema=None)

def sandman(
        application,
        database_uri,
        exclude_tables=None,
        user_models=None,
        reflect_all=True,
        read_only=False,
        schema=None):
    """..."""
    db.init_app(application)
    admin = Admin(application, base_template='layout.html', template_mode='bootstrap3')
    _register_error_handlers(application)
    if user_models:
        with application.app_context():
            _register_user_models(user_models, admin, schema=schema)
    elif reflect_all:
        with application.app_context():
            _reflect_all(exclude_tables, admin, read_only, schema=schema)

    @application.route('/')
    def index():
        """Return a list of routes to the registered classes."""
        routes = {}
        for cls in application.classes:
            routes[cls.__model__.__name__] = '{}{{/{}}}'.format(
                cls.__model__.__url__,
                cls.__model__.primary_key())
        return jsonify(routes)

Once this is done it becomes rather trivial to add security to the application.

from flask_jwt_extended import JWTManager

def create_app(
        database_uri,
        exclude_tables=None,
        user_models=None,
        reflect_all=True,
        read_only=False,
        schema=None):
    app = Flask('sandman2')
    app.config['SQLALCHEMY_DATABASE_URI'] = database_uri
    app.config['SANDMAN2_READ_ONLY'] = read_only
    app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
    app.classes = []
    jwt = JWTManager(app)
    sandman(
        application,
        database_uri,
        exclude_tables=None,
        user_models=None,
        reflect_all=True,
        read_only=False,
        schema=None)

Then add the endpoints one wants as necessary. Similarly it is possible to secure ones administration interface by adding say Flask-Login but one must also then subclass the AdminView and AdminIndexView and pass these through the the Admin invocation in the "new" sandman method.

I have submitted a PR that already does this refactoring and am awaiting its acceptance.

zeluspudding commented 4 years ago

Yes, authentication is a must. I believe sandman1 had it. Was expecting sandman2 to do it better... not less... But I as a beggar cannot be a boss. But we can choose another approach, such as dreamfactory. Will start playing with that now... hopefully it's not crazy hard to setup.

Carelvd commented 4 years ago

@zeluspudding My example above was meant to illustrate that forcing a security solution upon the user is unnecessary and that, with minor refactoring of the code, it becomes trivial for persons using the library to set this up.

To setup security layer for the interim one need only copy the create_app/get_app code and extend it yourself, as I have above. There are multiple libraries that deal with this e.g. flask_jwt, flask_jwt_extended, flask-login and the like.

@jeffknupp When my PR is accepted/rejected I will push up my documentation for the changes and I can happily document the methods by which security can be added.