Repository: baobabsoluciones/cornflow (language: python)

Issue:
Control dag-level permissions in cornflow / airflow
**Is your feature request related to a problem? Please describe.**
Sometimes I'm a user who should not have access to certain solvers.

**Describe the solution you'd like**
I would like to be able to assign dag permissions to roles so only certain users would have access to certain dags/solvers.

**Describe alternatives you've considered**
Maybe we could store that in airflow and then cornflow would then use the cornflow user to get the dag information from airflow?
https://airflow.apache.org/docs/apache-airflow/stable/security/access-control.html#dag-level-role


Migrate to flask 2.0.#
This migration need changes to `flask_migrate` and `flask_script`, so that the manager does work with the flask cli included in flask 2.0.#

Changes will be needed to be performed to the existing migrations and to the deployment scripts.


Discussion / hints:
Couldn't we implement this with user group? And the group the user belongs
to, indicates the dags the user have access to, that way they can not even
create an instance pointing to that schema.

On Wed, Jul 28, 2021, 19:39 Franco Peschiera ***@***.***>
wrote:

> *Is your feature request related to a problem? Please describe.*
> Sometimes I'm a user who should not have access to certain solvers.
>
> *Describe the solution you'd like*
> I would like to be able to assign dag permissions to roles so only certain
> users would have access to certain dags/solvers.
>
> *Describe alternatives you've considered*
> Maybe we could store that in airflow and then cornflow would then use the
> cornflow user to get the dag information from airflow?
>
> https://airflow.apache.org/docs/apache-airflow/stable/security/access-control.html#dag-level-role
>
> —
> You are receiving this because you are subscribed to this thread.
> Reply to this email directly, view it on GitHub
> <https://github.com/baobabsoluciones/corn/issues/80>, or unsubscribe
> <https://github.com/notifications/unsubscribe-auth/AF4DGHGN6ZS7O2A4PADT7RDT2A6GHANCNFSM5BE2G5KA>
> .
>

It's possible and it's true we have talked about it.

Currently, cornflow does not have a list of avaliable dags. If we decide to
keep the dags only in airflow, then maybe it makes sense to control access
there. That way cornflow asks airflow (as it does now) for information on
dags. This implies we would need to replicate cornflows roles in airflow.

If we decide to create a dag catalog in cornflow that is synced with
airflow, then we could give permission at the dag level.
This implies that we would have to replicate airflow dag information in
cornflow.

There are probably other factors that I'm not thinking of that are
relevant.

On Wed, Jul 28, 2021, 20:03 Guillermo González-Santander de la Cruz <
***@***.***> wrote:

> Couldn't we implement this with user group? And the group the user belongs
> to, indicates the dags the user have access to, that way they can not even
> create an instance pointing to that schema.
>
> On Wed, Jul 28, 2021, 19:39 Franco Peschiera ***@***.***>
> wrote:
>
> > *Is your feature request related to a problem? Please describe.*
> > Sometimes I'm a user who should not have access to certain solvers.
> >
> > *Describe the solution you'd like*
> > I would like to be able to assign dag permissions to roles so only
> certain
> > users would have access to certain dags/solvers.
> >
> > *Describe alternatives you've considered*
> > Maybe we could store that in airflow and then cornflow would then use the
> > cornflow user to get the dag information from airflow?
> >
> >
> https://airflow.apache.org/docs/apache-airflow/stable/security/access-control.html#dag-level-role
> >
> > —
> > You are receiving this because you are subscribed to this thread.
> > Reply to this email directly, view it on GitHub
> > <https://github.com/baobabsoluciones/corn/issues/80>, or unsubscribe
> > <
> https://github.com/notifications/unsubscribe-auth/AF4DGHGN6ZS7O2A4PADT7RDT2A6GHANCNFSM5BE2G5KA
> >
> > .
> >
>
> —
> You are receiving this because you authored the thread.
> Reply to this email directly, view it on GitHub
> <https://github.com/baobabsoluciones/corn/issues/80#issuecomment-888510814>,
> or unsubscribe
> <https://github.com/notifications/unsubscribe-auth/ABJUZ44DVJKHVOSA7ZP2VBDT2BA5PANCNFSM5BE2G5KA>
> .
>

Should start by checking that we can set up the flask server with all the commands, that these work and that the migration commands also work.

This work is being done in the branch feature/remove_requirement_versions

Which includes the bump to flask 2.0.# and other changes that come from the rest of version bumps. This changes should set up the Cornflow 3.0.0 version

Fix patch:
diff --git a/AUTHORS b/AUTHORS
index a71201e8..ff3e41ac 100644
--- a/AUTHORS
+++ b/AUTHORS
@@ -1,4 +1,5 @@
-Franco Peschiera
-Hugo Larzabal
 David Ampuero
+Franco Peschiera
 Guillermo González-Santander
+Hugo Larzabal
+Marion Cottard
\ No newline at end of file
diff --git a/README.rst b/README.rst
index 460490b7..4ed40e9e 100644
--- a/README.rst
+++ b/README.rst
@@ -46,12 +46,12 @@ Download the Cornflow project and install requirements::
 
     git clone git@github.com:baobabsoluciones/cornflow-server.git
     cd corn
-    python3 -m venv cfvenv
-    cfvenv/bin/pip3 install -r requirements-dev.txt
+    python3 -m venv venv
+    venv/bin/pip3 install -r requirements-dev.txt
 
 activate the virtual environment and run Cornflow::
 
-    source cfvenv/bin/activate
+    source venv/bin/activate
     export FLASK_APP=cornflow.app
     flask run
 
diff --git a/airflow_config/dags/model_functions.py b/airflow_config/dags/model_functions.py
index 5e4b7d4d..7895435e 100644
--- a/airflow_config/dags/model_functions.py
+++ b/airflow_config/dags/model_functions.py
@@ -1,4 +1,3 @@
-
 import cornflow_client.airflow.dag_utilities as utils
 from cornflow_client import get_pulp_jsonschema
 import pulp as pl
@@ -6,7 +5,7 @@
 import os
 
 
-name = 'solve_model_dag'
+name = "solve_model_dag"
 instance = solution = get_pulp_jsonschema()
 config = get_pulp_jsonschema("solver_config.json")
 
@@ -21,10 +20,10 @@ def solve(data, config):
     var, model = pl.LpProblem.from_dict(data)
 
     # we overwrite the logPath argument before solving.
-    log_path = config['logPath'] = 'temp.log'
-    config['msg'] = 0
-    if 'solver' not in config:
-        config['solver'] = 'PULP_CBC_CMD'
+    log_path = config["logPath"] = "temp.log"
+    config["msg"] = 0
+    if "solver" not in config:
+        config["solver"] = "PULP_CBC_CMD"
     try:
         solver = pl.getSolverFromDict(config)
     except pl.PulpSolverError:
@@ -39,21 +38,28 @@ def solve(data, config):
         log = f.read()
 
     # we convert the log into orloge json
-    equivs = \
-        dict(
-            CPLEX_CMD='CPLEX', CPLEX_PY='CPLEX', CPLEX_DLL='CPLEX',
-            GUROBI='GUROBI', GUROBI_CMD='GUROBI',
-            PULP_CBC_CMD='CBC', COIN_CMD='CBC'
-        )
+    equivs = dict(
+        CPLEX_CMD="CPLEX",
+        CPLEX_PY="CPLEX",
+        CPLEX_DLL="CPLEX",
+        GUROBI="GUROBI",
+        GUROBI_CMD="GUROBI",
+        PULP_CBC_CMD="CBC",
+        COIN_CMD="CBC",
+    )
     solver_name = equivs.get(solver.name)
     log_dict = None
     if solver_name:
         try:
-            log_dict = ol.get_info_solver(path=log, solver=solver_name, get_progress=True, content=True)
+            log_dict = ol.get_info_solver(
+                path=log, solver=solver_name, get_progress=True, content=True
+            )
         except:
             log_dict = dict()
         else:
-            log_dict['progress'] = log_dict['progress'].fillna('').to_dict(orient='list')
+            log_dict["progress"] = (
+                log_dict["progress"].fillna("").to_dict(orient="list")
+            )
     print("Log read")
 
     try:
diff --git a/airflow_config/dags/optim_dag.py b/airflow_config/dags/optim_dag.py
index 46065037..64096691 100644
--- a/airflow_config/dags/optim_dag.py
+++ b/airflow_config/dags/optim_dag.py
@@ -1,8 +1,10 @@
+# TODO: review if needed
 from airflow import DAG
 from airflow.operators.python import PythonOperator
 from airflow.secrets.environment_variables import EnvironmentVariablesBackend
 import model_functions as mf
 
+
 dag = DAG(mf.name, default_args=mf.utils.default_args, schedule_interval=None)
 
 
diff --git a/airflow_config/dags/update_all_schemas.py b/airflow_config/dags/update_all_schemas_v1.0.py
similarity index 71%
rename from airflow_config/dags/update_all_schemas.py
rename to airflow_config/dags/update_all_schemas_v1.0.py
index e608d5cf..106b8c63 100644
--- a/airflow_config/dags/update_all_schemas.py
+++ b/airflow_config/dags/update_all_schemas_v1.0.py
@@ -8,18 +8,18 @@
 
 
 default_args = {
-    'owner': 'baobab',
-    'depends_on_past': False,
-    'start_date': datetime(2020, 2, 1),
-    'email': [''],
-    'email_on_failure': False,
-    'email_on_retry': False,
-    'retries': -1,
-    'retry_delay': timedelta(minutes=1),
-    'schedule_interval': "@once",
+    "owner": "baobab",
+    "depends_on_past": False,
+    "start_date": datetime(2020, 2, 1),
+    "email": [""],
+    "email_on_failure": False,
+    "email_on_retry": False,
+    "retries": -1,
+    "retry_delay": timedelta(minutes=1),
+    "schedule_interval": "@once",
 }
 
-schemas = ['instance', 'solution', 'config']
+schemas = ["instance", "solution", "config"]
 
 
 def get_all_apps():
@@ -36,7 +36,7 @@ def _import_file(filename):
 
 def is_app(dag_module):
     filename, ext = os.path.splitext(dag_module)
-    if ext != '.py':
+    if ext != ".py":
         return False
     try:
         _module = _import_file(filename)
@@ -58,7 +58,9 @@ def get_all_schemas():
         print("Found the following apps: {}".format([app.name for app in apps]))
     else:
         print("No apps were found to update")
-    schemas = [(dag_module.name, get_schemas_dag_file(dag_module)) for dag_module in apps]
+    schemas = [
+        (dag_module.name, get_schemas_dag_file(dag_module)) for dag_module in apps
+    ]
     return schemas
 
 
@@ -68,15 +70,15 @@ def update_schemas(**kwargs):
         Variable.set(key=key, value=value, serialize_json=True)
 
 
-dag = DAG('update_all_schemas', default_args=default_args, catchup=False)
+dag = DAG("update_all_schemas_v1.0", default_args=default_args, catchup=False)
 
 update_schema2 = PythonOperator(
-    task_id='update_all_schemas',
+    task_id="update_all_schemas_v1.0",
     provide_context=True,
     python_callable=update_schemas,
     dag=dag,
 )
 
 
-if __name__ == '__main__':
+if __name__ == "__main__":
     update_schemas()
diff --git a/airflow_config/scripts/init_airflow_service.py b/airflow_config/scripts/init_airflow_service.py
index 782dc9f9..a9c5606d 100644
--- a/airflow_config/scripts/init_airflow_service.py
+++ b/airflow_config/scripts/init_airflow_service.py
@@ -1,5 +1,4 @@
 # Airflow init script for Dockerfile ENTRYPOINT
-from logging import error
 import os
 import subprocess
 import sys
@@ -24,8 +23,8 @@
     ("AIRFLOW_ROLE", "Admin"),
     ("AIRFLOW_PWD", "admin"),
     ("AIRFLOW_USER_EMAIL", "admin@example.com"),
-    ("CORNFLOW_SERVICE_USER", "serviceuser@cornflow.com"),
-    ("CORNFLOW_SERVICE_PWD", "servicecornflow1234"),
+    ("CORNFLOW_SERVICE_USER", "service_user@cornflow.com"),
+    ("CORNFLOW_SERVICE_PWD", "Serviceuser_1234"),
     ("AIRFLOW_LDAP_ENABLE", "False"),
 ]
 # update environ set
@@ -147,6 +146,7 @@
             "FATAL: if you set AIRFLOW_LDAP_USE_TLS you must also set AIRFLOW_LDAP_TLS_CA_CERTIFICATE"
         )
 
+
 # Entrypoint of airflow services depends on command given by arg
 def airflowsvc(afsvc):
     if afsvc == "webserver":
@@ -171,7 +171,7 @@ def airflowsvc(afsvc):
         os.system(f"airflow {afsvc}")
     if afsvc == "flower":
         time.sleep(10)
-        os.system(f"airflow celery {afsvc} --basic-auth={AIRFLOW_USER}:\"{AIRFLOW_PWD}\"")
+        os.system(f'airflow celery {afsvc} --basic-auth={AIRFLOW_USER}:"{AIRFLOW_PWD}"')
     else:
         os.system("airflow version")
 
diff --git a/airflow_config/scripts/solvers/get_cbc.py b/airflow_config/scripts/solvers/get_cbc.py
index b506b027..5119e9e5 100644
--- a/airflow_config/scripts/solvers/get_cbc.py
+++ b/airflow_config/scripts/solvers/get_cbc.py
@@ -25,7 +25,7 @@ def install():
         output_cbc = cbc_install.stdout
         print(output_cbc)
 
-    except (error):
+    except error:
 
         print(error)
 
diff --git a/airflow_config/scripts/solvers/get_choco.py b/airflow_config/scripts/solvers/get_choco.py
index 547701f5..f7aa0118 100644
--- a/airflow_config/scripts/solvers/get_choco.py
+++ b/airflow_config/scripts/solvers/get_choco.py
@@ -35,7 +35,7 @@ def install():
         choco_path = "/usr/local/bin/choco.jar"
         os.chown(choco_path, uid, gid)
 
-    except (error):
+    except error:
 
         print(error)
 
diff --git a/airflow_config/scripts/solvers/get_glpk.py b/airflow_config/scripts/solvers/get_glpk.py
index b4f2b15a..ca7b3e20 100644
--- a/airflow_config/scripts/solvers/get_glpk.py
+++ b/airflow_config/scripts/solvers/get_glpk.py
@@ -24,7 +24,7 @@ def install():
         output_glpk = glpk_install.stdout
         print(output_glpk)
 
-    except (error):
+    except error:
 
         print(error)
 
diff --git a/airflow_config/scripts/solvers/get_highs.py b/airflow_config/scripts/solvers/get_highs.py
index a8839f5c..9fc7240e 100644
--- a/airflow_config/scripts/solvers/get_highs.py
+++ b/airflow_config/scripts/solvers/get_highs.py
@@ -34,7 +34,7 @@ def install():
         highs_path = "/usr/local/bin/highs"
         os.chown(highs_path, uid, gid)
 
-    except (error):
+    except error:
 
         print(error)
 
diff --git a/airflow_config/scripts/solvers/get_mipcl.py b/airflow_config/scripts/solvers/get_mipcl.py
index 09d95ec4..bd622441 100644
--- a/airflow_config/scripts/solvers/get_mipcl.py
+++ b/airflow_config/scripts/solvers/get_mipcl.py
@@ -30,7 +30,7 @@ def install():
         mips_path = "/usr/local/bin/mps_mipcl"
         os.chown(mips_path, uid, gid)
 
-    except (error):
+    except error:
 
         print(error)
 
diff --git a/cornflow/app.py b/cornflow/app.py
index 060abb7d..438eebd4 100644
--- a/cornflow/app.py
+++ b/cornflow/app.py
@@ -1,15 +1,31 @@
+import click
 import os
 
 from flask import Flask
+from flask.cli import with_appcontext
 from flask_apispec.extension import FlaskApiSpec
 from flask_cors import CORS
+from flask_migrate import Migrate
 from flask_restful import Api
 
-
+from .commands.access import access_init_command
+from .commands.actions import register_actions_command
+from .commands.dag import register_deployed_dags_command
+from .commands.permissions import (
+    register_base_permissions_command,
+    register_dag_permissions_command,
+)
+from .commands.roles import register_roles_command
+from .commands.users import (
+    create_admin_user_command,
+    create_base_user_command,
+    create_service_user_command,
+)
+from .commands.views import register_views_command
 from .config import app_config
 from .endpoints import resources
 from .shared.compress import init_compress
-from .shared.exceptions import _initialize_errorhandlers
+from .shared.exceptions import initialize_errorhandlers
 from .shared.utils import db, bcrypt
 
 
@@ -17,6 +33,7 @@ def create_app(env_name="development", dataconn=None):
     """
 
     :param str env_name: 'testing' or 'development' or 'production'
+    :param str dataconn: string to connect to the database
     :return: the application that is going to be running :class:`Flask`
     :rtype: :class:`Flask`
     """
@@ -29,6 +46,7 @@ def create_app(env_name="development", dataconn=None):
     CORS(app)
     bcrypt.init_app(app)
     db.init_app(app)
+    migrate = Migrate(app=app, db=db)
 
     if "sqlite" in app.config["SQLALCHEMY_DATABASE_URI"]:
 
@@ -48,11 +66,106 @@ def _fk_pragma_on_connect(dbapi_con, con_record):
     for res in resources:
         docs.register(target=res["resource"], endpoint=res["endpoint"])
 
-    _initialize_errorhandlers(app)
+    initialize_errorhandlers(app)
     init_compress(app)
+
+    app.cli.add_command(create_service_user)
+    app.cli.add_command(create_admin_user)
+    app.cli.add_command(register_roles)
+    app.cli.add_command(create_base_user)
+    app.cli.add_command(register_actions)
+    app.cli.add_command(register_views)
+    app.cli.add_command(register_base_assignations)
+    app.cli.add_command(access_init)
+    app.cli.add_command(register_deployed_dags)
+    app.cli.add_command(register_dag_permissions)
+
     return app
 
 
+@click.command("create_service_user")
+@click.option("-u", "--username", required=True, type=str)
+@click.option("-e", "--email", required=True, type=str)
+@click.option("-p", "--password", required=True, type=str)
+@click.option("-v", "--verbose", default=0)
+@with_appcontext
+def create_service_user(username, email, password, verbose):
+    create_service_user_command(username, email, password, verbose)
+
+
+@click.command("create_admin_user")
+@click.option("-u", "--username", required=True, type=str)
+@click.option("-e", "--email", required=True, type=str)
+@click.option("-p", "--password", required=True, type=str)
+@click.option("-v", "--verbose", type=int, default=0)
+@with_appcontext
+def create_admin_user(username, email, password, verbose):
+    create_admin_user_command(username, email, password, verbose)
+
+
+@click.command("create_base_user")
+@click.option("-u", "--username", required=True, type=str)
+@click.option("-e", "--email", required=True, type=str)
+@click.option("-p", "--password", required=True, type=str)
+@click.option("-v", "--verbose", type=int, default=0)
+@with_appcontext
+def create_base_user(username, email, password, verbose):
+    create_base_user_command(username, email, password, verbose)
+
+
+@click.command("register_roles")
+@click.option("-v", "--verbose", type=int, default=0)
+@with_appcontext
+def register_roles(verbose):
+    register_roles_command(verbose)
+
+
+@click.command("register_actions")
+@click.option("-v", "--verbose", type=int, default=0)
+@with_appcontext
+def register_actions(verbose):
+    register_actions_command(verbose)
+
+
+@click.command("register_views")
+@click.option("-v", "--verbose", type=int, default=0)
+@with_appcontext
+def register_views(verbose):
+    register_views_command(verbose)
+
+
+@click.command("register_base_assignations")
+@click.option("-v", "--verbose", type=int, default=0)
+@with_appcontext
+def register_base_assignations(verbose):
+    register_base_permissions_command(verbose)
+
+
+@click.command("access_init")
+@click.option("-v", "--verbose", type=int, default=0)
+@with_appcontext
+def access_init(verbose):
+    access_init_command(verbose)
+
+
+@click.command("register_deployed_dags")
+@click.option("-r", "--url", type=str)
+@click.option("-u", "--username", type=str)
+@click.option("-p", "--password", type=str)
+@click.option("-v", "--verbose", type=int, default=0)
+@with_appcontext
+def register_deployed_dags(url, username, password, verbose):
+    register_deployed_dags_command(url, username, password, verbose)
+
+
+@click.command("register_dag_permissions")
+@click.option("-o", "--open_deployment", default=0, type=int)
+@click.option("-v", "--verbose", type=int, default=0)
+@with_appcontext
+def register_dag_permissions(open_deployment, verbose):
+    register_dag_permissions_command(open_deployment=open_deployment, verbose=verbose)
+
+
 if __name__ == "__main__":
     environment_name = os.getenv("FLASK_ENV", "development")
     # env_name = 'development'
diff --git a/cornflow/commands/__init__.py b/cornflow/commands/__init__.py
deleted file mode 100644
index 73ae2715..00000000
--- a/cornflow/commands/__init__.py
+++ /dev/null
@@ -1,14 +0,0 @@
-"""
-Initialization file for the commands module
-"""
-
-from .commands import (
-    AccessInitialization,
-    RegisterBasePermissions,
-    CreateAdminUser,
-    CreateServiceUser,
-    CleanHistoricData,
-    RegisterActions,
-    RegisterRoles,
-    RegisterViews,
-)
diff --git a/cornflow/commands/access.py b/cornflow/commands/access.py
new file mode 100644
index 00000000..6eabcc63
--- /dev/null
+++ b/cornflow/commands/access.py
@@ -0,0 +1,10 @@
+def access_init_command(verbose):
+    from .actions import register_actions_command
+    from .permissions import register_base_permissions_command
+    from .roles import register_roles_command
+    from .views import register_views_command
+
+    register_actions_command(verbose)
+    register_roles_command(verbose)
+    register_views_command(verbose)
+    register_base_permissions_command(verbose)
diff --git a/cornflow/commands/actions.py b/cornflow/commands/actions.py
new file mode 100644
index 00000000..460f466f
--- /dev/null
+++ b/cornflow/commands/actions.py
@@ -0,0 +1,38 @@
+def register_actions_command(verbose):
+    from sqlalchemy.exc import IntegrityError
+
+    from ..models import ActionModel
+    from ..shared.const import ACTIONS_MAP
+    from ..shared.utils import db
+
+    actions_registered = [ac.name for ac in ActionModel.get_all_objects()]
+
+    db.session.commit()
+
+    actions_to_register = [
+        ActionModel(id=key, name=value)
+        for key, value in ACTIONS_MAP.items()
+        if value not in actions_registered
+    ]
+
+    if len(actions_to_register) > 0:
+        db.session.bulk_save_objects(actions_to_register)
+
+    try:
+        db.session.commit()
+    except IntegrityError:
+        db.session.rollback()
+
+    if "postgres" in str(db.session.get_bind()):
+        db.engine.execute(
+            "SELECT setval(pg_get_serial_sequence('actions', 'id'), MAX(id)) FROM actions;"
+        )
+        db.session.commit()
+
+    if verbose == 1:
+        if len(actions_to_register) > 0:
+            print("Actions registered: ", actions_to_register)
+        else:
+            print("No new actions to be registered")
+
+    return True
diff --git a/cornflow/commands/cleanup.py b/cornflow/commands/cleanup.py
new file mode 100644
index 00000000..e69de29b
diff --git a/cornflow/commands/commands.py b/cornflow/commands/commands.py
deleted file mode 100644
index 1c0d1fb6..00000000
--- a/cornflow/commands/commands.py
+++ /dev/null
@@ -1,384 +0,0 @@
-"""
-File with the different defined commands
-"""
-# Import from libraries
-from flask_script import Command, Option
-
-# Import from internal modules
-from cornflow.models import (
-    ActionModel,
-    ApiViewModel,
-    PermissionViewRoleModel,
-    RoleModel,
-    UserModel,
-    UserRoleModel,
-)
-from cornflow.shared.const import (
-    ADMIN_ROLE,
-    ACTIONS_MAP,
-    BASE_PERMISSION_ASSIGNATION,
-    EXTRA_PERMISSION_ASSIGNATION,
-    ROLES_MAP,
-    SERVICE_ROLE,
-)
-from cornflow.endpoints import resources
-from cornflow.shared.utils import db
-
-username_option = Option(
-    "-u", "--username", dest="username", help="User username", type=str
-)
-email_option = Option("-e", "--email", dest="email", help="User email", type=str)
-password_option = Option(
-    "-p", "--password", dest="password", help="User password", type=str
-)
-
-verbose_option = Option(
-    "-v",
-    "--verbose",
-    dest="verbose",
-    help="Verbose for the command. 0 no verbose, 1 full verbose",
-    type=int,
-)
-
-
-def create_user_with_role(username, email, password, name, role, verbose=0):
-    """
-    Method to create a user with a given email, password and name
-
-    :param str username: username for the new user
-    :param str email: email for the new user
-    :param str password: password for the new user
-    :param str name: name for the new user
-    :param int role: role for the new user
-    :param int verbose: verbose of the function
-    :return: a boolean if the execution went well
-    :rtype: bool
-    """
-    user = UserModel.get_one_user_by_username(username)
-
-    if user is None:
-        data = dict(username=username, email=email, password=password)
-        user = UserModel(data=data)
-        user.save()
-        user_role = UserRoleModel({"user_id": user.id, "role_id": role})
-        user_role.save()
-        if verbose == 1:
-            print("{} is created and assigned service role".format(name))
-        return True
-
-    user_role = UserRoleModel.get_one_user(user.id)
-    user_actual_roles = [ur.role for ur in user_role]
-    if user_role is not None and RoleModel.get_one_object(role) in user_actual_roles:
-        if verbose == 1:
-            print("{} exists and already has service role assigned".format(name))
-        return True
-
-    user_role = UserRoleModel({"user_id": user.id, "role_id": role})
-    user_role.save()
-    if verbose == 1:
-        print("{} already exists and is assigned a service role".format(name))
-    return True
-
-
-class CreateServiceUser(Command):
-    """
-    Creates the initial super user that is used by airflow to write the results of the execution back
-    This command should only be used on deployment
-    """
-
-    def get_options(self):
-        return (
-            username_option,
-            email_option,
-            password_option,
-            verbose_option,
-        )
-
-    def run(self, username, email, password, verbose=0):
-        """
-        Method to run the command and create the service user
-
-        :param str username: the username for the service user
-        :param str email: the email for the service user
-        :param str password: the password for the service user
-        :param int verbose: verbose of the command
-        :return: a boolean if the execution went right
-        :rtype: bool
-        """
-        if username is None or email is None or password is None:
-            print("Missing required arguments")
-            return False
-        return create_user_with_role(
-            username, email, password, "serviceuser", SERVICE_ROLE, verbose
-        )
-
-
-class CreateAdminUser(Command):
-    def get_options(self):
-        return (
-            username_option,
-            email_option,
-            password_option,
-            verbose_option,
-        )
-
-    def run(self, username, email, password, verbose=0):
-        """
-        Method to run and create the admin user
-
-        :param str username: the username for the service user
-        :param str email: the email for the admin user
-        :param str password: the password for the admin user
-        :param int verbose: verbose of the command
-        :return: a boolean if the execution went right
-        :rtype: bool
-        """
-        if username is None or email is None or password is None:
-            print("Missing required arguments")
-            return False
-        return create_user_with_role(
-            username, email, password, "admin", ADMIN_ROLE, verbose
-        )
-
-
-class CleanHistoricData(Command):
-    """ """
-
-    # TODO: implement command to delete data than is older than X years (this time could be read from a settings file)
-    def run(self):
-        """
-
-        :return:
-        :rtype:
-        """
-        pass
-
-
-class RegisterActions(Command):
-    def get_options(self):
-        return (verbose_option,)
-
-    def run(self, verbose=0):
-        """
-        Method to register the actions on the database
-
-        :param int verbose: verbose of the command
-        :return: a boolean if the execution went right
-        :rtype: bool
-        """
-
-        actions_registered = [ac.name for ac in ActionModel.get_all_objects()]
-
-        db.session.commit()
-
-        actions_to_register = [
-            ActionModel(id=key, name=value)
-            for key, value in ACTIONS_MAP.items()
-            if value not in actions_registered
-        ]
-
-        if len(actions_to_register) > 0:
-            db.session.bulk_save_objects(actions_to_register)
-
-        db.session.commit()
-
-        if "postgres" in str(db.session.get_bind()):
-            db.engine.execute(
-                "SELECT setval(pg_get_serial_sequence('actions', 'id'), MAX(id)) FROM actions;"
-            )
-            db.session.commit()
-
-        if verbose == 1:
-            if len(actions_to_register) > 0:
-                print("Actions registered: ", actions_to_register)
-            else:
-                print("No new actions to be registered")
-
-        return True
-
-
-class RegisterViews(Command):
-    def get_options(self):
-        return (verbose_option,)
-
-    def run(self, verbose=0):
-        """
-        Method to register the endpoints in cornflow
-
-        :param int verbose: verbose of the command
-        :return: a boolean if the execution went right
-        :rtype: bool
-        """
-        views_registered = [view.name for view in ApiViewModel.get_all_objects()]
-
-        db.session.commit()
-
-        views_to_register = [
-            ApiViewModel(
-                {
-                    "name": view["endpoint"],
-                    "url_rule": view["urls"],
-                    "description": view["resource"].DESCRIPTION,
-                }
-            )
-            for view in resources
-            if view["endpoint"] not in views_registered
-        ]
-
-        if len(views_to_register) > 0:
-            db.session.bulk_save_objects(views_to_register)
-
-        db.session.commit()
-
-        if "postgres" in str(db.session.get_bind()):
-            db.engine.execute(
-                "SELECT setval(pg_get_serial_sequence('api_view', 'id'), MAX(id)) FROM api_view;"
-            )
-            db.session.commit()
-
-        if verbose == 1:
-            if len(views_to_register) > 0:
-                print("Endpoints registered: ", views_to_register)
-            else:
-                print("No new endpoints to be registered")
-
-        return True
-
-
-class RegisterRoles(Command):
-    def get_options(self):
-        return (verbose_option,)
-
-    def run(self, verbose=0):
-        """
-        Method to register the roles in cornflow
-
-        :param int verbose: verbose of the command
-        :return: a boolean if the execution went right
-        :rtype: bool
-        """
-        roles_registered = [role.name for role in RoleModel.get_all_objects()]
-
-        db.session.commit()
-
-        roles_to_register = [
-            RoleModel({"id": key, "name": value})
-            for key, value in ROLES_MAP.items()
-            if value not in roles_registered
-        ]
-
-        if len(roles_to_register) > 0:
-            db.session.bulk_save_objects(roles_to_register)
-
-        db.session.commit()
-
-        if "postgres" in str(db.session.get_bind()):
-            db.engine.execute(
-                "SELECT setval(pg_get_serial_sequence('roles', 'id'), MAX(id)) FROM roles;"
-            )
-            db.session.commit()
-
-        if verbose == 1:
-            if len(roles_to_register) > 0:
-                print("Roles registered: ", roles_to_register)
-            else:
-                print("No new roles to be registered")
-
-        return True
-
-
-class RegisterBasePermissions(Command):
-    def get_options(self):
-        return (verbose_option,)
-
-    def run(self, verbose=0):
-        """
-        Method to register the base permissions
-
-        :param int verbose: verbose of the command
-        :return: a boolean if the execution went right
-        :rtype: bool
-        """
-
-        permissions_registered = [
-            (perm.action_id, perm.api_view_id, perm.role_id)
-            for perm in PermissionViewRoleModel.get_all_objects()
-        ]
-
-        db.session.commit()
-        views = {view.name: view.id for view in ApiViewModel.get_all_objects()}
-
-        # Create base permissions
-        permissions_to_register = [
-            PermissionViewRoleModel(
-                {
-                    "role_id": role,
-                    "action_id": action,
-                    "api_view_id": views[view["endpoint"]],
-                }
-            )
-            for role, action in BASE_PERMISSION_ASSIGNATION
-            for view in resources
-            if role in view["resource"].ROLES_WITH_ACCESS
-            and (
-                action,
-                views[view["endpoint"]],
-                role,
-            )
-            not in permissions_registered
-        ] + [
-            PermissionViewRoleModel(
-                {
-                    "role_id": role,
-                    "action_id": action,
-                    "api_view_id": views[endpoint],
-                }
-            )
-            for role, action, endpoint in EXTRA_PERMISSION_ASSIGNATION
-            if (
-                action,
-                views[endpoint],
-                role,
-            )
-            not in permissions_registered
-        ]
-
-        if len(permissions_to_register) > 0:
-            db.session.bulk_save_objects(permissions_to_register)
-
-        db.session.commit()
-
-        if "postgres" in str(db.session.get_bind()):
-            db.engine.execute(
-                "SELECT setval(pg_get_serial_sequence('permission_view', 'id'), MAX(id)) FROM permission_view;"
-            )
-            db.session.commit()
-
-        if verbose == 1:
-            if len(permissions_to_register) > 0:
-                print("Permissions registered: ", permissions_to_register)
-            else:
-                print("No new permissions to register")
-
-        return True
-
-
-class AccessInitialization(Command):
-    def get_options(self):
-        return (verbose_option,)
-
-    def run(self, verbose=0):
-        """
-        Method to run all access commands together
-
-        :param int verbose: verbose of the command
-        :return: a boolean if the execution went right
-        :rtype: bool
-        """
-        RegisterActions().run(verbose)
-        RegisterViews().run(verbose)
-        RegisterRoles().run(verbose)
-        RegisterBasePermissions().run(verbose)
-        if verbose == 1:
-            print("Access initialization ran successfully")
-        return True
diff --git a/cornflow/commands/dag.py b/cornflow/commands/dag.py
new file mode 100644
index 00000000..c333e9b0
--- /dev/null
+++ b/cornflow/commands/dag.py
@@ -0,0 +1,65 @@
+def register_deployed_dags_command(
+    url: str = None, user: str = None, pwd: str = None, verbose: int = 0
+):
+    import time
+
+    #
+    from cornflow_client.airflow.api import Airflow
+
+    from sqlalchemy.exc import IntegrityError
+    from ..models import DeployedDAG
+    from ..shared.utils import db
+
+    af_client = Airflow(url, user, pwd)
+    max_attempts = 20
+    attempts = 0
+    while not af_client.is_alive() and attempts < max_attempts:
+        attempts += 1
+        if verbose == 1:
+            print(f"Airflow is not reachable (attempt {attempts})")
+        time.sleep(15)
+
+    if not af_client.is_alive():
+        if verbose == 1:
+            print("Airflow is not reachable")
+        return False
+
+    dags_registered = [dag.id for dag in DeployedDAG.get_all_objects()]
+
+    response = af_client.get_model_dags()
+    dag_list = response.json()["dags"]
+
+    processed_dags = [
+        DeployedDAG({"id": dag["dag_id"], "description": dag["description"]})
+        for dag in dag_list
+        if dag["dag_id"] not in dags_registered
+    ]
+
+    if len(processed_dags) > 0:
+        db.session.bulk_save_objects(processed_dags)
+
+    try:
+        db.session.commit()
+    except IntegrityError as e:
+        db.session.rollback()
+
+    if verbose == 1:
+        if len(processed_dags) > 0:
+            print(f"DAGs registered: {processed_dags}")
+        else:
+            print("No new DAGs")
+    return True
+
+
+def register_deployed_dags_command_test(dags: list = None, verbose=0):
+    from ..models import DeployedDAG
+
+    if dags is None:
+        dags = ["solve_model_dag", "gc", "timer"]
+
+    deployed_dag = [DeployedDAG({"id": dag, "description": None}) for dag in dags]
+    for dag in deployed_dag:
+        dag.save()
+
+    if verbose == 1:
+        print("Registered DAGs")
diff --git a/cornflow/commands/permissions.py b/cornflow/commands/permissions.py
new file mode 100644
index 00000000..9c14f88a
--- /dev/null
+++ b/cornflow/commands/permissions.py
@@ -0,0 +1,125 @@
+def register_base_permissions_command(verbose):
+    from sqlalchemy.exc import IntegrityError
+
+    from ..endpoints import resources
+    from ..models import ApiViewModel, PermissionViewRoleModel
+    from ..shared.const import BASE_PERMISSION_ASSIGNATION, EXTRA_PERMISSION_ASSIGNATION
+    from ..shared.utils import db
+
+    permissions_registered = [
+        (perm.action_id, perm.api_view_id, perm.role_id)
+        for perm in PermissionViewRoleModel.get_all_objects()
+    ]
+
+    db.session.commit()
+    views = {view.name: view.id for view in ApiViewModel.get_all_objects()}
+
+    # Create base permissions
+    permissions_to_register = [
+        PermissionViewRoleModel(
+            {
+                "role_id": role,
+                "action_id": action,
+                "api_view_id": views[view["endpoint"]],
+            }
+        )
+        for role, action in BASE_PERMISSION_ASSIGNATION
+        for view in resources
+        if role in view["resource"].ROLES_WITH_ACCESS
+        and (
+            action,
+            views[view["endpoint"]],
+            role,
+        )
+        not in permissions_registered
+    ] + [
+        PermissionViewRoleModel(
+            {
+                "role_id": role,
+                "action_id": action,
+                "api_view_id": views[endpoint],
+            }
+        )
+        for role, action, endpoint in EXTRA_PERMISSION_ASSIGNATION
+        if (
+            action,
+            views[endpoint],
+            role,
+        )
+        not in permissions_registered
+    ]
+
+    if len(permissions_to_register) > 0:
+        db.session.bulk_save_objects(permissions_to_register)
+
+    try:
+        db.session.commit()
+    except IntegrityError:
+        db.session.rollback()
+
+    if "postgres" in str(db.session.get_bind()):
+        db.engine.execute(
+            "SELECT setval(pg_get_serial_sequence('permission_view', 'id'), MAX(id)) FROM permission_view;"
+        )
+        db.session.commit()
+
+    if verbose == 1:
+        if len(permissions_to_register) > 0:
+            print("Permissions registered: ", permissions_to_register)
+        else:
+            print("No new permissions to register")
+
+    return True
+
+
+def register_dag_permissions_command(open_deployment: int = None, verbose: int = 0):
+
+    from flask import current_app
+    from sqlalchemy.exc import IntegrityError
+
+    from ..models import DeployedDAG, PermissionsDAG, UserModel
+    from ..shared.utils import db
+
+    if open_deployment is None:
+        open_deployment = int(current_app.config["OPEN_DEPLOYMENT"])
+
+    existing_permissions = [
+        (permission.dag_id, permission.user_id)
+        for permission in PermissionsDAG.get_all_objects()
+    ]
+
+    db.session.commit()
+    all_users = UserModel.get_all_users()
+    all_dags = DeployedDAG.get_all_objects()
+
+    if open_deployment == 1:
+        permissions = [
+            PermissionsDAG({"dag_id": dag.id, "user_id": user.id})
+            for user in all_users
+            for dag in all_dags
+            if (dag.id, user.id) not in existing_permissions
+        ]
+
+    else:
+        permissions = [
+            PermissionsDAG({"dag_id": dag.id, "user_id": user.id})
+            for user in all_users
+            for dag in all_dags
+            if (dag.id, user.id) not in existing_permissions and user.is_service_user()
+        ]
+
+    if len(permissions) > 1:
+        db.session.bulk_save_objects(permissions)
+
+    try:
+        db.session.commit()
+    except IntegrityError:
+        db.session.rollback()
+
+    if verbose == 1:
+        if len(permissions) > 1:
+            print(f"DAG permissions registered: {permissions}")
+        else:
+            print("No new DAG permissions")
+
+    pass
diff --git a/cornflow/commands/roles.py b/cornflow/commands/roles.py
new file mode 100644
index 00000000..0da6c88a
--- /dev/null
+++ b/cornflow/commands/roles.py
@@ -0,0 +1,37 @@
+def register_roles_command(verbose):
+    from sqlalchemy.exc import IntegrityError
+
+    from ..models import RoleModel
+    from ..shared.const import ROLES_MAP
+    from ..shared.utils import db
+
+    roles_registered = [role.name for role in RoleModel.get_all_objects()]
+    db.session.commit()
+
+    roles_to_register = [
+        RoleModel({"id": key, "name": value})
+        for key, value in ROLES_MAP.items()
+        if value not in roles_registered
+    ]
+
+    if len(roles_to_register) > 0:
+        db.session.bulk_save_objects(roles_to_register)
+
+    try:
+        db.session.commit()
+    except IntegrityError:
+        db.session.rollback()
+
+    if "postgres" in str(db.session.get_bind()):
+        db.engine.execute(
+            "SELECT setval(pg_get_serial_sequence('roles', 'id'), MAX(id)) FROM roles;"
+        )
+        db.session.commit()
+
+    if verbose:
+        if len(roles_to_register) > 0:
+            print("Roles registered: ", roles_to_register)
+        else:
+            print("No new roles to be registered")
+
+    return True
diff --git a/cornflow/commands/schemas.py b/cornflow/commands/schemas.py
new file mode 100644
index 00000000..ecf99c51
--- /dev/null
+++ b/cornflow/commands/schemas.py
@@ -0,0 +1,33 @@
+def update_schemas_command(url, user, pwd, verbose):
+    import time
+
+    #
+    from cornflow_client.airflow.api import Airflow
+
+    from sqlalchemy.exc import IntegrityError
+    from ..models import DeployedDAG
+    from ..shared.utils import db
+
+    af_client = Airflow(url, user, pwd)
+    max_attempts = 20
+    attempts = 0
+    while not af_client.is_alive() and attempts < max_attempts:
+        attempts += 1
+        if verbose == 1:
+            print(f"Airflow is not reachable (attempt {attempts})")
+        time.sleep(15)
+
+    if not af_client.is_alive():
+        if verbose == 1:
+            print("Airflow is not reachable")
+        return False
+
+    response = af_client.update_schemas()
+    if response.status_code == 200:
+        if verbose == 1:
+            print("DAGs schemas updated")
+    else:
+        if verbose == 1:
+            print("The DAGs schemas were not updated properly")
+
+    return True
diff --git a/cornflow/commands/users.py b/cornflow/commands/users.py
new file mode 100644
index 00000000..308e6f1a
--- /dev/null
+++ b/cornflow/commands/users.py
@@ -0,0 +1,60 @@
+def create_user_with_role(username, email, password, name, role, verbose=0):
+    from ..models import RoleModel, UserModel, UserRoleModel
+
+    user = UserModel.get_one_user_by_username(username)
+
+    if user is None:
+        data = dict(username=username, email=email, password=password)
+        user = UserModel(data=data)
+        user.save()
+        user_role = UserRoleModel({"user_id": user.id, "role_id": role})
+        user_role.save()
+        if verbose == 1:
+            print("{} is created and assigned service role".format(name))
+        return True
+
+    user_role = UserRoleModel.get_one_user(user.id)
+    user_actual_roles = [ur.role for ur in user_role]
+    if user_role is not None and RoleModel.get_one_object(role) in user_actual_roles:
+        if verbose == 1:
+            print("{} exists and already has service role assigned".format(name))
+        return True
+
+    user_role = UserRoleModel({"user_id": user.id, "role_id": role})
+    user_role.save()
+    if verbose == 1:
+        print("{} already exists and is assigned a service role".format(name))
+    return True
+
+
+def create_service_user_command(username, email, password, verbose):
+    from ..shared.const import SERVICE_ROLE
+
+    if username is None or email is None or password is None:
+        print("Missing required arguments")
+        return False
+    return create_user_with_role(
+        username, email, password, "serviceuser", SERVICE_ROLE, verbose
+    )
+
+
+def create_admin_user_command(username, email, password, verbose):
+    from ..shared.const import ADMIN_ROLE
+
+    if username is None or email is None or password is None:
+        print("Missing required arguments")
+        return False
+    return create_user_with_role(
+        username, email, password, "admin", ADMIN_ROLE, verbose
+    )
+
+
+def create_base_user_command(username, email, password, verbose):
+    from ..shared.const import VIEWER_ROLE
+
+    if username is None or email is None or password is None:
+        print("Missing required arguments")
+        return False
+    return create_user_with_role(
+        username, email, password, username, VIEWER_ROLE, verbose
+    )
diff --git a/cornflow/commands/views.py b/cornflow/commands/views.py
new file mode 100644
index 00000000..5923cf50
--- /dev/null
+++ b/cornflow/commands/views.py
@@ -0,0 +1,44 @@
+def register_views_command(verbose):
+    from sqlalchemy.exc import IntegrityError
+
+    from ..endpoints import resources
+    from ..models import ApiViewModel
+    from ..shared.utils import db
+
+    views_registered = [view.name for view in ApiViewModel.get_all_objects()]
+
+    db.session.commit()
+
+    views_to_register = [
+        ApiViewModel(
+            {
+                "name": view["endpoint"],
+                "url_rule": view["urls"],
+                "description": view["resource"].DESCRIPTION,
+            }
+        )
+        for view in resources
+        if view["endpoint"] not in views_registered
+    ]
+
+    if len(views_to_register) > 0:
+        db.session.bulk_save_objects(views_to_register)
+
+    try:
+        db.session.commit()
+    except IntegrityError:
+        db.session.rollback()
+
+    if "postgres" in str(db.session.get_bind()):
+        db.engine.execute(
+            "SELECT setval(pg_get_serial_sequence('api_view', 'id'), MAX(id)) FROM api_view;"
+        )
+        db.session.commit()
+
+    if verbose == 1:
+        if len(views_to_register) > 0:
+            print("Endpoints registered: ", views_to_register)
+        else:
+            print("No new endpoints to be registered")
+
+    return True
diff --git a/cornflow/config.py b/cornflow/config.py
index 57b46929..e53d4170 100644
--- a/cornflow/config.py
+++ b/cornflow/config.py
@@ -16,6 +16,9 @@ class DefaultConfig(object):
     DEBUG = True
     TESTING = True
 
+    # Open deployment (all dags accessible to all users)
+    OPEN_DEPLOYMENT = os.getenv("OPEN_DEPLOYMENT", 1)
+
     # LDAP configuration
     LDAP_HOST = os.getenv("LDAP_HOST", "ldap://openldap:389")
     LDAP_BIND_DN = os.getenv("LDAP_BIND_DN", "cn=admin,dc=example,dc=org")
diff --git a/cornflow/endpoints/__init__.py b/cornflow/endpoints/__init__.py
index 31a680c6..9c26baca 100644
--- a/cornflow/endpoints/__init__.py
+++ b/cornflow/endpoints/__init__.py
@@ -15,7 +15,7 @@
     CaseToInstance,
     CaseCompare,
 )
-from .dag import DAGEndpoint, DAGEndpointManual
+from .dag import DAGEndpoint, DAGEndpointManual, DeployedDAGEndpoint
 
 from .execution import (
     ExecutionEndpoint,
@@ -86,6 +86,7 @@
     dict(resource=ExecutionEndpoint, urls="/execution/", endpoint="execution"),
     dict(resource=DAGEndpoint, urls="/dag/<string:idx>/", endpoint="dag"),
     dict(resource=DAGEndpointManual, urls="/dag/", endpoint="dag-manual"),
+    dict(resource=DeployedDAGEndpoint, urls="/dag/deployed/", endpoint="deployed-dag"),
     dict(resource=UserEndpoint, urls="/user/", endpoint="user"),
     dict(
         resource=UserDetailsEndpoint,
diff --git a/cornflow/endpoints/action.py b/cornflow/endpoints/action.py
index fef9b693..601c2b22 100644
--- a/cornflow/endpoints/action.py
+++ b/cornflow/endpoints/action.py
@@ -10,7 +10,7 @@
 from ..models import ActionModel
 from ..schemas.action import ActionsResponse
 from ..shared.authentication import Auth
-from ..shared.const import ADMIN_ROLE, SERVICE_ROLE
+from ..shared.const import ADMIN_ROLE
 
 
 class ActionListEndpoint(MetaResource, MethodResource):
diff --git a/cornflow/endpoints/apiview.py b/cornflow/endpoints/apiview.py
index 7f1b7585..c74719bc 100644
--- a/cornflow/endpoints/apiview.py
+++ b/cornflow/endpoints/apiview.py
@@ -10,7 +10,7 @@
 from ..models import ApiViewModel
 from ..schemas.apiview import ApiViewResponse
 from ..shared.authentication import Auth
-from ..shared.const import ADMIN_ROLE, SERVICE_ROLE
+from ..shared.const import ADMIN_ROLE
 
 
 class ApiViewListEndpoint(MetaResource, MethodResource):
diff --git a/cornflow/endpoints/case.py b/cornflow/endpoints/case.py
index ee209f59..abc918ed 100644
--- a/cornflow/endpoints/case.py
+++ b/cornflow/endpoints/case.py
@@ -59,6 +59,7 @@ def get(self, **kwargs):
 
     @doc(description="Create a new case from raw data", tags=["Cases"])
     @Auth.auth_required
+    @Auth.dag_permission_required
     @inflate
     @marshal_with(CaseListResponse)
     @use_kwargs(CaseRawRequest, location="json")
@@ -79,6 +80,7 @@ class CaseFromInstanceExecutionEndpoint(MetaResource, MethodResource):
 
     @doc(description="Create a new case from instance and execution", tags=["Cases"])
     @Auth.auth_required
+    @Auth.dag_permission_required
     @marshal_with(CaseListResponse)
     @use_kwargs(CaseFromInstanceExecution, location="json")
     def post(self, **kwargs):
@@ -382,6 +384,8 @@ def get(self, idx1, idx2, **kwargs):
             payload["solution_patch"] = jsonpatch.make_patch(
                 case_1.solution, case_2.solution
             ).patch
+
+        payload["schema"] = case_1.schema
         log.debug(
             "User {} compared cases {} and {}".format(self.get_user_id(), idx1, idx2)
         )
diff --git a/cornflow/endpoints/dag.py b/cornflow/endpoints/dag.py
index 59cfe0ab..b35ffedd 100644
--- a/cornflow/endpoints/dag.py
+++ b/cornflow/endpoints/dag.py
@@ -12,7 +12,8 @@
 
 # Import from internal modules
 from .meta_resource import MetaResource
-from ..models import ExecutionModel, InstanceModel
+from ..models import DeployedDAG, ExecutionModel, InstanceModel
+from ..schemas import DeployedDAGSchema
 from ..schemas.execution import (
     ExecutionDagPostRequest,
     ExecutionDagRequest,
@@ -152,3 +153,28 @@ def post(self, **kwargs):
             )
         )
         return item, 201
+
+
+class DeployedDAGEndpoint(MetaResource, MethodResource):
+    ROLES_WITH_ACCESS = [SERVICE_ROLE]
+
+    def __init__(self):
+        super().__init__()
+        self.model = DeployedDAG
+
+    @doc(
+        description="Get list of deployed dags registered on the data base",
+        tags=["DeployedDAGs"],
+    )
+    @Auth.auth_required
+    @marshal_with(DeployedDAGSchema(many=True))
+    def get(self, **kwargs):
+        return self.model.get_all_objects()
+
+    @doc(description="Post a new deployed dag", tags=["DeployedDAGs"])
+    @Auth.auth_required
+    @marshal_with(DeployedDAGSchema)
+    @use_kwargs(DeployedDAGSchema)
+    def post(self, **kwargs):
+        response = self.post_list(kwargs)
+        return response
diff --git a/cornflow/endpoints/execution.py b/cornflow/endpoints/execution.py
index dcdb07f3..05ee8ffc 100644
--- a/cornflow/endpoints/execution.py
+++ b/cornflow/endpoints/execution.py
@@ -76,6 +76,7 @@ def get(self, **kwargs):
 
     @doc(description="Create an execution", tags=["Executions"])
     @Auth.auth_required
+    @Auth.dag_permission_required
     @marshal_with(ExecutionDetailsEndpointResponse)
     @use_kwargs(ExecutionRequest, location="json")
     def post(self, **kwargs):
@@ -198,7 +199,7 @@ def get(self, idx):
 
     @doc(description="Edit an execution", tags=["Executions"], inherit=False)
     @Auth.auth_required
-    @use_kwargs(ExecutionEditRequest, location=("json"))
+    @use_kwargs(ExecutionEditRequest, location="json")
     def put(self, idx, **data):
         """
         Edit an existing execution
@@ -229,6 +230,7 @@ def delete(self, idx):
 
     @doc(description="Stop an execution", tags=["Executions"], inherit=False)
     @Auth.auth_required
+    @Auth.dag_permission_required
     def post(self, idx):
         execution = ExecutionModel.get_one_object_from_user(
             user=self.get_user(), idx=idx
diff --git a/cornflow/endpoints/instance.py b/cornflow/endpoints/instance.py
index 760ffad0..512ddc88 100644
--- a/cornflow/endpoints/instance.py
+++ b/cornflow/endpoints/instance.py
@@ -67,6 +67,7 @@ def get(self, **kwargs):
 
     @doc(description="Create an instance", tags=["Instances"])
     @Auth.auth_required
+    @Auth.dag_permission_required
     @inflate
     @marshal_with(InstanceDetailsEndpointResponse)
     @use_kwargs(InstanceRequest, location="json")
@@ -136,6 +137,7 @@ def get(self, idx):
 class InstanceDetailsEndpoint(InstanceDetailsEndpointBase):
     @doc(description="Edit an instance", tags=["Instances"])
     @Auth.auth_required
+    @Auth.dag_permission_required
     @use_kwargs(InstanceEditRequest, location="json")
     def put(self, idx, **data):
         """
diff --git a/cornflow/endpoints/login.py b/cornflow/endpoints/login.py
index b933e4da..42c651f8 100644
--- a/cornflow/endpoints/login.py
+++ b/cornflow/endpoints/login.py
@@ -3,24 +3,23 @@
 """
 
 # Import from libraries
+import logging as log
+
 from flask import g, current_app
 from flask_apispec import use_kwargs, doc
 from flask_apispec.views import MethodResource
-import logging as log
+from sqlalchemy.exc import IntegrityError
 
 # Import from internal modules
 from .meta_resource import MetaResource
 from ..models import UserModel, UserRoleModel
-from ..schemas.user import UserSchema, LoginEndpointRequest
+from ..schemas.user import LoginEndpointRequest
 from ..shared.authentication import Auth
 from ..shared.const import AUTH_DB, AUTH_LDAP
 from ..shared.exceptions import InvalidUsage, InvalidCredentials
 from ..shared.ldap import LDAP
 from ..shared.utils import db
 
-# Initialize the schema that the endpoint uses
-user_schema = UserSchema()
-
 
 class LoginEndpoint(MetaResource, MethodResource):
     """
@@ -87,6 +86,7 @@ def auth_ldap_authenticate(username, password):
         # regardless whether the user is new or not:
         # we update the roles it has according to ldap
         roles = ldap_obj.get_user_roles(username)
+
         try:
             # we first remove all roles for the user
             UserRoleModel.del_one_user(user.id)
@@ -96,7 +96,9 @@ def auth_ldap_authenticate(username, password):
                 db.session.add(user_role)
             # we only commit if everything went well
             db.session.commit()
-        except:
+        except IntegrityError as e:
+            log.error("Foreign keys error")
+            log.error(e)
             # or we rollback
             db.session.rollback()
         return user
diff --git a/cornflow/endpoints/permission.py b/cornflow/endpoints/permission.py
index 953af21b..4a02ab5f 100644
--- a/cornflow/endpoints/permission.py
+++ b/cornflow/endpoints/permission.py
@@ -3,8 +3,7 @@
 """
 
 # Import from libraries
-from flask import current_app
-from flask_apispec import marshal_with, use_kwargs, doc
+from flask_apispec import doc, marshal_with, use_kwargs
 from flask_apispec.views import MethodResource
 import logging as log
 
@@ -18,8 +17,8 @@
 )
 from ..shared.authentication import Auth
 from ..shared.compress import compressed
-from ..shared.const import ADMIN_ROLE, AUTH_LDAP
-from ..shared.exceptions import EndpointNotImplemented, ObjectAlreadyExists
+from ..shared.const import ADMIN_ROLE
+from ..shared.exceptions import ObjectAlreadyExists
 
 
 class PermissionsViewRoleEndpoint(MetaResource, MethodResource):
diff --git a/cornflow/endpoints/schemas.py b/cornflow/endpoints/schemas.py
index fb293a1e..b4831c79 100644
--- a/cornflow/endpoints/schemas.py
+++ b/cornflow/endpoints/schemas.py
@@ -4,15 +4,17 @@
 
 # Import from libraries
 from cornflow_client.airflow.api import Airflow
-from flask import current_app
+from flask import current_app, request
 from flask_apispec.views import MethodResource
-from flask_apispec import marshal_with, use_kwargs, doc
+from flask_apispec import marshal_with, doc
 import logging as log
 
 # Import from internal modules
 from .meta_resource import MetaResource
-from ..shared.exceptions import AirflowError
-from ..schemas.schemas import SchemaOneApp, SchemaRequest, SchemaListApp
+from ..models import PermissionsDAG
+from ..shared.authentication import Auth
+from ..shared.exceptions import AirflowError, NoPermission
+from ..schemas.schemas import SchemaOneApp, SchemaListApp
 
 
 class SchemaEndpoint(MetaResource, MethodResource):
@@ -21,6 +23,7 @@ class SchemaEndpoint(MetaResource, MethodResource):
     """
 
     @doc(description="Get list of available apps", tags=["Schemas"])
+    @Auth.auth_required
     @marshal_with(SchemaListApp(many=True))
     def get(self):
         """
@@ -29,13 +32,12 @@ def get(self):
         :return: A dictionary with a message and a integer with the HTTP status code
         :rtype: Tuple(dict, integer)
         """
-        af_client = Airflow.from_config(current_app.config)
-        if not af_client.is_alive():
-            log.error("Airflow not accessible when getting schemas")
-            raise AirflowError(error="Airflow is not accessible")
+        user = Auth.get_user_obj_from_header(request.headers)
+        dags = PermissionsDAG.get_user_dag_permissions(user.id)
+        available_dags = [{"name": dag.dag_id} for dag in dags]
 
         log.debug("User gets list of schema")
-        return af_client.get_all_schemas()
+        return available_dags
 
 
 class SchemaDetailsEndpoint(MetaResource, MethodResource):
@@ -44,6 +46,7 @@ class SchemaDetailsEndpoint(MetaResource, MethodResource):
     """
 
     @doc(description="Get instance, solution and config schema", tags=["Schemas"])
+    @Auth.auth_required
     @marshal_with(SchemaOneApp)
     def get(self, dag_name):
         """
@@ -52,14 +55,27 @@ def get(self, dag_name):
         :return: A dictionary with a message and a integer with the HTTP status code
         :rtype: Tuple(dict, integer)
         """
-        af_client = Airflow.from_config(current_app.config)
-        if not af_client.is_alive():
-            log.error("Airflow not accessible when getting schema {}".format(dag_name))
-            raise AirflowError(error="Airflow is not accessible")
+        user = Auth.get_user_obj_from_header(request.headers)
+        permission = PermissionsDAG.check_if_has_permissions(
+            user_id=user.id, dag_id=dag_name
+        )
 
-        # try airflow and see if dag_name exists
-        af_client.get_dag_info(dag_name)
+        if permission:
+            af_client = Airflow.from_config(current_app.config)
+            if not af_client.is_alive():
+                log.error(
+                    "Airflow not accessible when getting schema {}".format(dag_name)
+                )
+                raise AirflowError(error="Airflow is not accessible")
 
-        log.debug("User gets schema {}".format(dag_name))
-        # it exists: we try to get its schemas
-        return af_client.get_schemas_for_dag_name(dag_name)
+            # try airflow and see if dag_name exists
+            af_client.get_dag_info(dag_name)
+
+            log.debug("User gets schema {}".format(dag_name))
+            # it exists: we try to get its schemas
+            return af_client.get_schemas_for_dag_name(dag_name)
+        else:
+            raise NoPermission(
+                error="User does not have permission to access this dag",
+                status_code=403,
+            )
diff --git a/cornflow/endpoints/signup.py b/cornflow/endpoints/signup.py
index 5ea02ae1..f16b7c06 100644
--- a/cornflow/endpoints/signup.py
+++ b/cornflow/endpoints/signup.py
@@ -9,7 +9,7 @@
 
 # Import from internal modules
 from .meta_resource import MetaResource
-from ..models import UserModel, UserRoleModel
+from ..models import UserModel, PermissionsDAG, UserRoleModel
 from ..schemas.user import UserSignupRequest
 from ..shared.authentication import Auth
 from ..shared.const import AUTH_LDAP, PLANNER_ROLE
@@ -47,12 +47,27 @@ def post(self, **kwargs):
                 error="Email already in use, please supply another email address"
             )
 
+        check_pwd = UserModel.check_password_pattern(kwargs.get('password'))
+        if not check_pwd["valid"]:
+            raise InvalidCredentials(
+                error=check_pwd["message"]
+            )
+
+        check_email = UserModel.check_email_pattern(kwargs.get("email"))
+        if not check_email["valid"]:
+            raise InvalidCredentials(
+                error=check_email["message"]
+            )
+
         user = UserModel(kwargs)
         user.save()
 
         user_role = UserRoleModel({"user_id": user.id, "role_id": PLANNER_ROLE})
         user_role.save()
 
+        if int(current_app.config["OPEN_DEPLOYMENT"]) == 1:
+            PermissionsDAG.add_all_permissions_to_user(user.id)
+
         try:
             token = Auth.generate_token(user.id)
         except Exception as e:
diff --git a/cornflow/endpoints/user.py b/cornflow/endpoints/user.py
index a1143023..a6736c85 100644
--- a/cornflow/endpoints/user.py
+++ b/cornflow/endpoints/user.py
@@ -25,6 +25,7 @@
     ObjectDoesNotExist,
     NoPermission,
     EndpointNotImplemented,
+    InvalidCredentials
 )
 from ..shared.utils import db
 
@@ -125,6 +126,20 @@ def put(self, user_id, **data):
         # in ldap-mode, users cannot be edited.
         if current_app.config["AUTH_TYPE"] == AUTH_LDAP and user_obj.comes_from_ldap():
             raise EndpointNotImplemented("To edit a user, go to LDAP server")
+
+        if data.get('password'):
+            check_pwd = UserModel.check_password_pattern(data.get('password'))
+            if not check_pwd["valid"]:
+                raise InvalidCredentials(
+                    error=check_pwd["message"]
+                )
+
+        if data.get('email'):
+            check_email = UserModel.check_email_pattern(data.get("email"))
+            if not check_email["valid"]:
+                raise InvalidCredentials(
+                    error=check_email["message"]
+                )
         user_obj.update(data)
         user_obj.save()
         log.info("User {} was edited by user {}".format(user_id, self.get_user_id()))
diff --git a/cornflow/gunicorn.py b/cornflow/gunicorn.py
index baa50f23..1f3be9fb 100644
--- a/cornflow/gunicorn.py
+++ b/cornflow/gunicorn.py
@@ -2,9 +2,11 @@
 import os
 from multiprocessing import cpu_count
 
+
 def max_workers():
     return cpu_count()
 
+
 pidfile = "/usr/src/app/gunicorn.pid"
 chdir = "/usr/src/app"
 bind = "0.0.0.0:5000"
diff --git a/cornflow/models/__init__.py b/cornflow/models/__init__.py
index b07f2158..448a0895 100644
--- a/cornflow/models/__init__.py
+++ b/cornflow/models/__init__.py
@@ -4,8 +4,9 @@
 from .action import ActionModel
 from .apiview import ApiViewModel
 from .case import CaseModel
+from .dag import DeployedDAG
 from .execution import ExecutionModel
 from .instance import InstanceModel
-from .permission import PermissionViewRoleModel
+from .permission import PermissionsDAG, PermissionViewRoleModel
 from .roles import RoleModel, UserRoleModel
 from .user import UserModel
diff --git a/cornflow/models/action.py b/cornflow/models/action.py
index e5472d1f..093ea818 100644
--- a/cornflow/models/action.py
+++ b/cornflow/models/action.py
@@ -21,9 +21,22 @@ class ActionModel(EmptyModel):
     id = db.Column(db.Integer, primary_key=True, autoincrement=True)
     name = db.Column(db.String(128), unique=True, nullable=False)
 
+    permissions = db.relationship(
+        "PermissionViewRoleModel",
+        backref="actions",
+        lazy=True,
+        primaryjoin="and_(ActionModel.id==PermissionViewRoleModel.action_id, "
+        "PermissionViewRoleModel.deleted_at==None)",
+        cascade="all,delete",
+    )
+
     def __repr__(self):
         return str(self.id) + ": " + self.name
 
     @staticmethod
     def get_all_objects():
         return ActionModel.query.all()
+
+    @staticmethod
+    def get_one_object(idx):
+        return ActionModel.query.get(idx)
diff --git a/cornflow/models/apiview.py b/cornflow/models/apiview.py
index 8365b7c8..58f925da 100644
--- a/cornflow/models/apiview.py
+++ b/cornflow/models/apiview.py
@@ -20,6 +20,15 @@ class ApiViewModel(EmptyModel):
     url_rule = db.Column(db.String(128), nullable=False)
     description = db.Column(TEXT, nullable=True)
 
+    permissions = db.relationship(
+        "PermissionViewRoleModel",
+        backref="api_views",
+        lazy=True,
+        primaryjoin="and_(ApiViewModel.id==PermissionViewRoleModel.api_view_id, "
+        "PermissionViewRoleModel.deleted_at==None)",
+        cascade="all,delete",
+    )
+
     def __init__(self, data):
         super().__init__()
         self.name = data.get("name")
diff --git a/cornflow/models/dag.py b/cornflow/models/dag.py
new file mode 100644
index 00000000..def8f4ac
--- /dev/null
+++ b/cornflow/models/dag.py
@@ -0,0 +1,38 @@
+"""
+
+"""
+# Import from libraries
+from sqlalchemy.dialects.postgresql import TEXT
+
+# Import from internal modules
+from .meta_model import TraceAttributes
+from ..shared.utils import db
+
+
+class DeployedDAG(TraceAttributes):
+    """
+    This model contains the registry of the DAGs that are deployed on the corresponding Airflow server
+    """
+
+    __tablename__ = "deployed_dags"
+    id = db.Column(db.String(128), primary_key=True)
+    description = db.Column(TEXT, nullable=True)
+
+    dag_permissions = db.relationship(
+        "PermissionsDAG",
+        cascade="all,delete",
+        backref="deployed_dags",
+        primaryjoin="and_(DeployedDAG.id==PermissionsDAG.dag_id)",
+    )
+
+    def __init__(self, data):
+        super().__init__()
+        self.id = data.get("id")
+        self.description = data.get("description", None)
+
+    def __repr__(self):
+        return self.id
+
+    @staticmethod
+    def get_all_objects():
+        return DeployedDAG.query.all()
diff --git a/cornflow/models/permission.py b/cornflow/models/permission.py
index 95d9caaf..84be6fee 100644
--- a/cornflow/models/permission.py
+++ b/cornflow/models/permission.py
@@ -1,4 +1,5 @@
 from .meta_model import TraceAttributes
+from .dag import DeployedDAG
 from ..shared.utils import db
 
 
@@ -9,13 +10,13 @@ class PermissionViewRoleModel(TraceAttributes):
     id = db.Column(db.Integer, primary_key=True, autoincrement=True)
 
     action_id = db.Column(db.Integer, db.ForeignKey("actions.id"), nullable=False)
-    action = db.relationship("ActionModel")
+    action = db.relationship("ActionModel", viewonly=True)
 
-    api_view_id = db.Column(db.Integer, db.ForeignKey("api_view.id"), nullable=True)
-    api_view = db.relationship("ApiViewModel")
+    api_view_id = db.Column(db.Integer, db.ForeignKey("api_view.id"), nullable=False)
+    api_view = db.relationship("ApiViewModel", viewonly=True)
 
     role_id = db.Column(db.Integer, db.ForeignKey("roles.id"), nullable=False)
-    role = db.relationship("RoleModel")
+    role = db.relationship("RoleModel", viewonly=True)
 
     def __init__(self, data):
         super().__init__()
@@ -61,3 +62,50 @@ def get_all_objects():
 
     def __repr__(self):
         return "{} can {} on {}".format(self.role_id, self.action_id, self.api_view_id)
+
+
+class PermissionsDAG(TraceAttributes):
+    __tablename__ = "permission_dag"
+    __table_args__ = (db.UniqueConstraint("dag_id", "user_id"),)
+
+    id = db.Column(db.Integer, primary_key=True, autoincrement=True)
+
+    dag_id = db.Column(
+        db.String(128), db.ForeignKey("deployed_dags.id"), nullable=False
+    )
+    user_id = db.Column(db.Integer, db.ForeignKey("users.id"), nullable=False)
+    user = db.relationship("UserModel", viewonly=True)
+
+    def __init__(self, data):
+        super().__init__()
+        self.dag_id = data.get("dag_id")
+        self.user_id = data.get("user_id")
+
+    def __repr__(self):
+        return f"User {self.user_id} can access {self.dag_id}"
+
+    @staticmethod
+    def get_all_objects():
+        return PermissionsDAG.query.all()
+
+    @staticmethod
+    def get_user_dag_permissions(user_id):
+        return PermissionsDAG.query.filter_by(user_id=user_id).all()
+
+    @staticmethod
+    def add_all_permissions_to_user(user_id):
+        dags = DeployedDAG.get_all_objects()
+        permissions = [
+            PermissionsDAG({"dag_id": dag.id, "user_id": user_id}) for dag in dags
+        ]
+        for permission in permissions:
+            permission.save()
+
+    @staticmethod
+    def check_if_has_permissions(user_id, dag_id):
+        permission = PermissionsDAG.query.filter_by(
+            user_id=user_id, dag_id=dag_id
+        ).first()
+        if permission is None:
+            return False
+        return True
diff --git a/cornflow/models/roles.py b/cornflow/models/roles.py
index 57f27b80..e282f4f2 100644
--- a/cornflow/models/roles.py
+++ b/cornflow/models/roles.py
@@ -98,10 +98,10 @@ class UserRoleModel(TraceAttributes):
 
     id = db.Column(db.Integer, primary_key=True, autoincrement=True)
     user_id = db.Column(db.Integer, db.ForeignKey("users.id"), nullable=False)
-    user = db.relationship("UserModel")
+    user = db.relationship("UserModel", viewonly=True)
 
     role_id = db.Column(db.Integer, db.ForeignKey("roles.id"), nullable=False)
-    role = db.relationship("RoleModel")
+    role = db.relationship("RoleModel", viewonly=True)
 
     def __init__(self, data):
         """
@@ -239,7 +239,12 @@ def __repr__(self):
         :return: the representation
         :rtype: str
         """
-        return self.user.username + " has role " + self.role.name
+        try:
+            assignation = self.user.username + " has role " + self.role.name
+            return assignation
+        except AttributeError:
+            assignation = str(self.user_id) + "has role" + str(self.role_id)
+            return assignation
 
     def __str__(self):
         """
@@ -248,4 +253,9 @@ def __str__(self):
         :return: the string representation
         :rtype: str
         """
-        return self.user.username + " has role " + self.role.name
+        try:
+            assignation = self.user.username + " has role " + self.role.name
+            return assignation
+        except AttributeError:
+            assignation = str(self.user_id) + " has role " + str(self.role_id)
+            return assignation
diff --git a/cornflow/models/user.py b/cornflow/models/user.py
index 4803b554..ea43e108 100644
--- a/cornflow/models/user.py
+++ b/cornflow/models/user.py
@@ -1,5 +1,5 @@
-# Imports from sqlalchemy
-from sqlalchemy.sql import expression
+# Imports from libraries
+import re
 
 # Imports from internal modules
 from .meta_model import TraceAttributes
@@ -61,6 +61,13 @@ class UserModel(TraceAttributes):
 
     user_roles = db.relationship("UserRoleModel", cascade="all,delete", backref="users")
 
+    dag_permissions = db.relationship(
+        "PermissionsDAG",
+        cascade="all,delete",
+        backref="users",
+        primaryjoin="and_(UserModel.id==PermissionsDAG.user_id)",
+    )
+
     @property
     def roles(self):
         return {r.role.id: r.role.name for r in self.user_roles}
@@ -207,6 +214,55 @@ def check_email_in_use(email):
         """
         return UserModel.query.filter_by(email=email).first() is not None
 
+    @staticmethod
+    def check_password_pattern(password: str):
+        """
+        Checks if a password is valid
+        :param password: the password to check
+        :return: a dictionary containing: a boolean indicating if the password is valid, and a message
+        :rtype: dict
+        """
+        if len(password) < 5:
+            return {
+                "valid": False,
+                "message": "Password must contain at least 5. characters",
+            }
+        if password.islower() or password.isupper():
+            return {
+                "valid": False,
+                "message": "Password must contain uppercase and lowercase letters",
+            }
+        if len(list(filter(str.isdigit, password))) == 0:
+            return {
+                "valid": False,
+                "message": "Password must contain at least one number and one special character",
+            }
+
+        def is_special_character(character):
+            return character in [
+                char for char in "!¡?¿#$%&'()*+-_./:;,<>=@[]^`{}|~\"\\"
+            ]
+
+        if len(list(filter(is_special_character, password))) == 0:
+            return {
+                "valid": False,
+                "message": "Password must contain at least one number and one special character",
+            }
+        return {"valid": True, "message": ""}
+
+    @staticmethod
+    def check_email_pattern(email: str):
+        """
+        Checks if an email address is valid
+        :param email: The email to validate
+        :return: A dictionary containing: a boolean indicating if the email address is valid, and a message
+        :rtype: dict
+        """
+        email_pattern = r"\b[A-Za-z0-9._-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b"
+        if re.match(email_pattern, email) is None:
+            return {"valid": False, "message": "Invalid email address"}
+        return {"valid": True, "message": ""}
+
     def __repr__(self):
         """
         Representation method of the class
diff --git a/cornflow/schemas/__init__.py b/cornflow/schemas/__init__.py
index ca754f8b..3149416a 100644
--- a/cornflow/schemas/__init__.py
+++ b/cornflow/schemas/__init__.py
@@ -2,8 +2,10 @@
 Initialization file for the schemas module
 """
 
+from .dag import DeployedDAGSchema
 from .execution import ExecutionSchema
 from .instance import InstanceSchema
 from .user import UserSchema
 
+
 # from .model_json import DataSchema
diff --git a/cornflow/schemas/case.py b/cornflow/schemas/case.py
index 74a2a88b..dc1559fe 100644
--- a/cornflow/schemas/case.py
+++ b/cornflow/schemas/case.py
@@ -60,6 +60,7 @@ class CaseFromInstanceExecution(Schema):
     name = fields.Str(required=True)
     description = fields.Str()
     parent_id = fields.Int(allow_none=True)
+    schema = fields.Str(required=True)
 
 
 class CaseToInstanceResponse(Schema):
@@ -72,7 +73,6 @@ class CaseToInstanceResponse(Schema):
 class CaseEditRequest(Schema):
     name = fields.Str()
     description = fields.Str()
-    schema = fields.Str()
 
 
 class CaseCompareResponse(Schema):
diff --git a/cornflow/schemas/dag.py b/cornflow/schemas/dag.py
new file mode 100644
index 00000000..9a408863
--- /dev/null
+++ b/cornflow/schemas/dag.py
@@ -0,0 +1,8 @@
+from marshmallow import fields, Schema
+
+
+class DeployedDAGSchema(Schema):
+    """"""
+
+    id = fields.Str(required=True)
+    description = fields.Str(allow_none=True)
diff --git a/cornflow/shared/authentication.py b/cornflow/shared/authentication.py
index a03a29f3..b89f664a 100644
--- a/cornflow/shared/authentication.py
+++ b/cornflow/shared/authentication.py
@@ -3,9 +3,14 @@
 from functools import wraps
 import jwt
 
-from ..models import ApiViewModel, UserModel, UserRoleModel, PermissionViewRoleModel
+from ..models import ApiViewModel, UserModel, PermissionsDAG, PermissionViewRoleModel
 from ..shared.const import PERMISSION_METHOD_MAP
-from ..shared.exceptions import InvalidCredentials, ObjectDoesNotExist, NoPermission
+from ..shared.exceptions import (
+    InvalidCredentials,
+    InvalidData,
+    NoPermission,
+    ObjectDoesNotExist,
+)
 
 
 class Auth:
@@ -22,9 +27,7 @@ def generate_token(user_id):
             "sub": user_id,
         }
 
-        return jwt.encode(payload, current_app.config["SECRET_KEY"], "HS256").decode(
-            "utf8"
-        )
+        return jwt.encode(payload, current_app.config["SECRET_KEY"], "HS256")
 
     @staticmethod
     def decode_token(token):
@@ -89,7 +92,10 @@ def get_permission_for_request(req, user_id):
         method, url = Auth.get_request_info(req)
         user_roles = UserModel.get_one_user(user_id).roles
         if user_roles is None or user_roles == {}:
-            raise NoPermission("You do not have permission to access this endpoint")
+            raise NoPermission(
+                error="You do not have permission to access this endpoint",
+                status_code=403,
+            )
 
         action_id = PERMISSION_METHOD_MAP[method]
         view_id = ApiViewModel.query.filter_by(url_rule=url).first().id
@@ -102,9 +108,10 @@ def get_permission_for_request(req, user_id):
             if has_permission:
                 return True
 
-        raise NoPermission("You do not have permission to access this endpoint")
+        raise NoPermission(
+            error="You do not have permission to access this endpoint", status_code=403
+        )
 
-    # user decorator
     @staticmethod
     def auth_required(func):
         """
@@ -122,6 +129,38 @@ def decorated_user(*args, **kwargs):
 
         return decorated_user
 
+    @staticmethod
+    def dag_permission_required(func):
+        """
+        DAG permission decorator
+        :param func:
+        :return:
+        """
+
+        @wraps(func)
+        def dag_decorator(*args, **kwargs):
+            if int(current_app.config["OPEN_DEPLOYMENT"]) == 0:
+                user_id = Auth.get_user_obj_from_header(request.headers).id
+                dag_id = request.json.get("schema", None)
+                if dag_id is None:
+                    raise InvalidData(
+                        error="The request does not specify a schema to use",
+                        status_code=400,
+                    )
+                else:
+                    if PermissionsDAG.check_if_has_permissions(user_id, dag_id):
+                        # We have permissions
+                        return func(*args, **kwargs)
+                    else:
+                        raise NoPermission(
+                            error="You do not have permission to use this DAG",
+                            status_code=403,
+                        )
+            else:
+                return func(*args, **kwargs)
+
+        return dag_decorator
+
     @staticmethod
     def return_user_from_token(token):
         user_id = Auth.decode_token(token)["user_id"]
diff --git a/cornflow/shared/const.py b/cornflow/shared/const.py
index c315f969..be3e35fd 100644
--- a/cornflow/shared/const.py
+++ b/cornflow/shared/const.py
@@ -97,4 +97,5 @@
     (VIEWER_ROLE, PUT_ACTION, "user-detail"),
     (PLANNER_ROLE, POST_ACTION, "dag-manual"),
     (SERVICE_ROLE, POST_ACTION, "dag-manual"),
+    (SERVICE_ROLE, POST_ACTION, "deployed-dag"),
 ]
diff --git a/cornflow/shared/exceptions.py b/cornflow/shared/exceptions.py
index afec9da6..f5d21be5 100644
--- a/cornflow/shared/exceptions.py
+++ b/cornflow/shared/exceptions.py
@@ -45,7 +45,7 @@ class InvalidPatch(InvalidUsage):
     error = "The json patch sent is not valid"
 
 
-def _initialize_errorhandlers(app):
+def initialize_errorhandlers(app):
     @app.errorhandler(InvalidUsage)
     @app.errorhandler(ObjectDoesNotExist)
     @app.errorhandler(NoPermission)
diff --git a/cornflow/shared/ldap.py b/cornflow/shared/ldap.py
index 317f4cb0..a8031f9c 100644
--- a/cornflow/shared/ldap.py
+++ b/cornflow/shared/ldap.py
@@ -115,7 +115,6 @@ def get_user_roles(self, user):
         # first element is the cn string, the second is the USER_BASE
         # we only want the cn string
         roles = [el.cn[0] for el in conn.entries]
-
         env_to_role = {
             "LDAP_GROUP_TO_ROLE_" + str.upper(ROLES_MAP[k]): k
             for k in ALL_DEFAULT_ROLES
diff --git a/docker-compose-airflow-celery-separate.yml b/docker-compose-airflow-celery-separate.yml
index 1666dcaa..38b88aac 100644
--- a/docker-compose-airflow-celery-separate.yml
+++ b/docker-compose-airflow-celery-separate.yml
@@ -13,7 +13,7 @@ services:
       - AIRFLOW_USER=admin
       - AIRFLOW_PWD=admin
       - CORNFLOW_SERVICE_USER=service_user@cornflow.com
-      - CORNFLOW_SERVICE_PWD=serviceuser1234
+      - CORNFLOW_SERVICE_PWD=Serviceuser_1234
       - AIRFLOW_DB_HOST=airflow_db
       - AIRFLOW_DB_PORT=5432
       - AIRFLOW_DB_USER=airflow
@@ -47,7 +47,7 @@ services:
       - AIRFLOW_USER=admin
       - AIRFLOW_PWD=admin
       - CORNFLOW_SERVICE_USER=service_user@cornflow.com
-      - CORNFLOW_SERVICE_PWD=serviceuser1234
+      - CORNFLOW_SERVICE_PWD=Serviceuser_1234
       - AIRFLOW_DB_HOST=airflow_db
       - AIRFLOW_DB_PORT=5432
       - AIRFLOW_DB_USER=airflow
diff --git a/docker-compose-cornflow-celery.yml b/docker-compose-cornflow-celery.yml
index b7da3c6e..6869d539 100644
--- a/docker-compose-cornflow-celery.yml
+++ b/docker-compose-cornflow-celery.yml
@@ -10,7 +10,7 @@ services:
       - 5000:5000
     environment:
       - CORNFLOW_ADMIN_EMAIL=cornflow_admin@cornflow.com
-      - CORNFLOW_ADMIN_PWD=cornflowadmin1234
+      - CORNFLOW_ADMIN_PWD=Cornflowadmin_1234
       - CORNFLOW_DB_HOST=cornflow_db
       - CORNFLOW_DB_PORT=5432
       - CORNFLOW_DB_USER=cornflow
diff --git a/docker-compose-cornflow-separate.yml b/docker-compose-cornflow-separate.yml
index 035b0339..a0c4e325 100644
--- a/docker-compose-cornflow-separate.yml
+++ b/docker-compose-cornflow-separate.yml
@@ -10,9 +10,9 @@ services:
       - 5000:5000
     environment:
       - CORNFLOW_ADMIN_EMAIL=cornflow_admin@cornflow.com
-      - CORNFLOW_ADMIN_PWD=cornflowadmin1234
+      - CORNFLOW_ADMIN_PWD=Cornflowadmin_1234
       - CORNFLOW_SERVICE_USER=service_user@cornflow.com
-      - CORNFLOW_SERVICE_PWD=serviceuser1234
+      - CORNFLOW_SERVICE_PWD=Serviceuser_1234
       - AIRFLOW_USER=admin
       - AIRFLOW_PWD=admin
       - CORNFLOW_DB_HOST=cornflow_db
diff --git a/docker-compose.yml b/docker-compose.yml
index 61e9daa4..28a4b1d5 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -10,9 +10,9 @@ services:
       - 5000:5000
     environment:
       - CORNFLOW_ADMIN_EMAIL=cornflow_admin@cornflow.com
-      - CORNFLOW_ADMIN_PWD=cornflowadmin1234
+      - CORNFLOW_ADMIN_PWD=Cornflowadmin_1234
       - CORNFLOW_SERVICE_USER=service_user@cornflow.com
-      - CORNFLOW_SERVICE_PWD=serviceuser1234
+      - CORNFLOW_SERVICE_PWD=Serviceuser_1234
       - CORNFLOW_DB_HOST=cornflow_db
       - CORNFLOW_DB_PORT=5432
       - CORNFLOW_DB_USER=cornflow
@@ -43,7 +43,7 @@ services:
       - AIRFLOW_USER=admin
       - AIRFLOW_PWD=admin
       - CORNFLOW_SERVICE_USER=service_user@cornflow.com
-      - CORNFLOW_SERVICE_PWD=serviceuser1234
+      - CORNFLOW_SERVICE_PWD=Serviceuser_1234
       - AIRFLOW_DB_HOST=airflow_db
       - AIRFLOW_DB_PORT=5432
       - AIRFLOW_DB_USER=airflow
diff --git a/docs/source/AUTHORS.txt b/docs/source/AUTHORS.txt
index 8c49a633..2aada3fc 100644
--- a/docs/source/AUTHORS.txt
+++ b/docs/source/AUTHORS.txt
@@ -1,5 +1,6 @@
 David Ampuero
+David López
+Franco Peschiera
 Guillermo González-Santander
 Hugo Larzabal
-David Lopez
-Franco Peschiera
+Marion Cottard
\ No newline at end of file
diff --git a/docs/source/conf.py b/docs/source/conf.py
index 1fd52e70..bca25ba8 100644
--- a/docs/source/conf.py
+++ b/docs/source/conf.py
@@ -11,48 +11,50 @@
 # All configuration values have a default; values that are commented out
 # serve to show the default.
 
-import sys, os
+import os
+import sys
+
 
 # If extensions (or modules to document with autodoc) are in another directory,
 # add these directories to sys.path here. If the directory is relative to the
 # documentation root, use os.path.abspath to make it absolute, like shown here.
-sys.path.insert(0,os.path.abspath('../../'))
+sys.path.insert(0, os.path.abspath("../../"))
 
 # -- General configuration -----------------------------------------------------
 
 # Add any Sphinx extension module names here, as strings. They can be extensions
 # coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
 extensions = [
-    'sphinx.ext.autodoc',
-    'sphinx.ext.doctest',
-    'sphinx.ext.intersphinx',
-    'sphinx.ext.todo',
-    'sphinx.ext.coverage',
-    'sphinx.ext.ifconfig',
-    'sphinx.ext.autosummary',
-    'sphinx.ext.mathjax',
-    'sphinxcontrib.redoc',
-    'sphinx.ext.autosectionlabel'
-    ]
+    "sphinx.ext.autodoc",
+    "sphinx.ext.doctest",
+    "sphinx.ext.intersphinx",
+    "sphinx.ext.todo",
+    "sphinx.ext.coverage",
+    "sphinx.ext.ifconfig",
+    "sphinx.ext.autosummary",
+    "sphinx.ext.mathjax",
+    "sphinxcontrib.redoc",
+    "sphinx.ext.autosectionlabel",
+]
 # Add any paths that contain templates here, relative to this directory.
-templates_path = ['_templates']
+templates_path = ["_templates"]
 
 pngmath_use_preview = True
 todo_include_todos = True
 
-autoclass_content = 'both'
+autoclass_content = "both"
 # The suffix of source filenames.
-source_suffix = '.rst'
+source_suffix = ".rst"
 
 # The encoding of source files.
-#source_encoding = 'utf-8'
+# source_encoding = 'utf-8'
 
 # The master toctree document.
-master_doc = 'index'
+master_doc = "index"
 
 # General information about the project.
-project = 'Cornflow'
-copyright = '2020-, cornflow documentation team.'
+project = "Cornflow"
+copyright = "2020-, cornflow documentation team."
 
 # The version info for the project you're documenting, acts as replacement for
 # |version| and |release|, also used in various other places throughout the
@@ -63,23 +65,23 @@
 import cornflow_client
 
 # release = pulp.VERSION
-release = '0.4'
+release = "0.4"
 # The short X.Y version.
 # version = release[:3]
-version = '0.4'
+version = "0.4"
 
 # The language for content autogenerated by Sphinx. Refer to documentation
 # for a list of supported languages.
-#language = None
+# language = None
 
 # There are two options for replacing |today|: either, you set today to some
 # non-false value, then it is used:
-#today = ''
+# today = ''
 # Else, today_fmt is used as the format for a strftime call.
-#today_fmt = '%B %d, %Y'
+# today_fmt = '%B %d, %Y'
 
 # List of documents that shouldn't be included in the build.
-#unused_docs = []
+# unused_docs = []
 
 # List of directories, relative to source directory, that shouldn't be searched
 # for source files.
@@ -91,24 +93,24 @@
 exclude_patterns = []
 
 # The reST default role (used for this markup: `text`) to use for all documents.
-#default_role = None
+# default_role = None
 
 # If true, '()' will be appended to :func: etc. cross-reference text.
 add_function_parentheses = True
 
 # If true, the current module name will be prepended to all description
 # unit titles (such as .. function::).
-#add_module_names = True
+# add_module_names = True
 
 # If true, sectionauthor and moduleauthor directives will be shown in the
 # output. They are ignored by default.
-#show_authors = False
+# show_authors = False
 
 # The name of the Pygments (syntax highlighting) style to use.
-pygments_style = 'sphinx'
+pygments_style = "sphinx"
 
 # A list of ignored prefixes for module index sorting.
-#modindex_common_prefix = []
+# modindex_common_prefix = []
 
 
 # -- Options for HTML output ---------------------------------------------------
@@ -123,9 +125,9 @@
 # Theme options are theme-specific and customize the look and feel of a theme
 # further.  For a list of options available for each theme, see the
 # documentation.
-#html_theme_options = {}
+# html_theme_options = {}
 html_theme_options = {
-    # 'fixed_sidebar': True, 
+    # 'fixed_sidebar': True,
     # 'stickysidebar': True,
     # 'github_banner': True,
     # 'show_powered_by': False,
@@ -133,75 +135,75 @@
     # 'github_repo': 'corn'
 }
 # Add any paths that contain custom themes here, relative to this directory.
-#html_theme_path = []
+# html_theme_path = []
 
 # The name for this set of Sphinx documents.  If None, it defaults to
 # "<project> v<release> documentation".
-#html_title = None
+# html_title = None
 
 # A shorter title for the navigation bar.  Default is the same as html_title.
-#html_short_title = None
+# html_short_title = None
 
 # The name of an image file (relative to this directory) to place at the top
 # of the sidebar.
-#html_logo = None
+# html_logo = None
 
 # The name of an image file (within the static path) to use as favicon of the
 # docs.  This file should be a Windows icon file (.ico) being 16x16 or 32x32
 # pixels large.
-#html_favicon = None
+# html_favicon = None
 
 # Add any paths that contain custom static files (such as style sheets) here,
 # relative to this directory. They are copied after the builtin static files,
 # so a file named "default.css" will overwrite the builtin "default.css".
-html_static_path = ['_static']
+html_static_path = ["_static"]
 
 # If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
 # using the given strftime format.
-#html_last_updated_fmt = '%b %d, %Y'
+# html_last_updated_fmt = '%b %d, %Y'
 
 # If true, SmartyPants will be used to convert quotes and dashes to
 # typographically correct entities.
-#html_use_smartypants = True
+# html_use_smartypants = True
 
 # Custom sidebar templates, maps document names to template names.
-#html_sidebars = {}
+# html_sidebars = {}
 
 # Additional templates that should be rendered to pages, maps page names to
 # template names.
-#html_additional_pages = {}
+# html_additional_pages = {}
 
 # If false, no module index is generated.
-#html_use_modindex = True
+# html_use_modindex = True
 
 # If false, no index is generated.
-#html_use_index = True
+# html_use_index = True
 
 # If true, the index is split into individual pages for each letter.
-#html_split_index = False
+# html_split_index = False
 
 # If true, links to the reST sources are added to the pages.
-#html_show_sourcelink = True
+# html_show_sourcelink = True
 
 # If true, an OpenSearch description file will be output, and all pages will
 # contain a <link> tag referring to it.  The value of this option must be the
 # base URL from which the finished HTML is served.
-#html_use_opensearch = ''
+# html_use_opensearch = ''
 
 # If nonempty, this is the file name suffix for HTML files (e.g. ".xhtml").
-#html_file_suffix = ''
+# html_file_suffix = ''
 
 # Output file base name for HTML help builder.
-htmlhelp_basename = 'corndoc'
+htmlhelp_basename = "corndoc"
 
 
 # -- Options for LaTeX output --------------------------------------------------
 
 # The paper size ('letter' or 'a4').
-#latex_paper_size = 'letter'
+# latex_paper_size = 'letter'
 
 # The font size ('10pt', '11pt' or '12pt').
-#latex_font_size = '10pt'
+# latex_font_size = '10pt'
 
 # Grouping the document tree into LaTeX files. List of tuples
 # (source start file, target name, title, author, documentclass [howto/manual]).
@@ -209,39 +211,39 @@
 
 # The name of an image file (relative to this directory) to place at the top of
 # the title page.
-#latex_logo = None
+# latex_logo = None
 
 # For "manual" documents, if this is true, then toplevel headings are parts,
 # not chapters.
-#latex_use_parts = False
+# latex_use_parts = False
 
 # Additional stuff for the LaTeX preamble.
-#latex_preamble = ''
+# latex_preamble = ''
 
 # Documents to append as an appendix to all manuals.
-#latex_appendices = []
+# latex_appendices = []
 
 # If false, no module index is generated.
-#latex_use_modindex = True
+# latex_use_modindex = True
 
 
 # getting the OPENAPI file instructions:
 # 1. Launch the flask app (assuming defaults)
 # 2. curl http://127.0.0.1:5000/swagger/ > cornflow/static/v1.json
 
-intersphinx_mapping = {'http://docs.python.org/3': None}
+intersphinx_mapping = {"http://docs.python.org/3": None}
 
 OPENAPI_FILE = os.path.join(
     os.path.dirname(__file__), "..", "..", "cornflow", "static", "v1.json"
 )
 redoc = [
     {
-        'name': 'Cornflow REST API',
-        'page': 'stable-rest-api-ref',
-        'spec': OPENAPI_FILE,
-        'opts': {
-            'hide-hostname': True,
-            'no-auto-auth': True,
+        "name": "Cornflow REST API",
+        "page": "stable-rest-api-ref",
+        "spec": OPENAPI_FILE,
+        "opts": {
+            "hide-hostname": True,
+            "no-auto-auth": True,
         },
     },
-]
\ No newline at end of file
+]
diff --git a/docs/source/deploy/access.rst b/docs/source/deploy/access.rst
index 13f4c186..59b9989b 100644
--- a/docs/source/deploy/access.rst
+++ b/docs/source/deploy/access.rst
@@ -163,7 +163,7 @@ In the cornflow image, if no environment variables are set, an admin user is cre
 
     CORNFLOW_ADMIN_USER - cornflow_admin
     CORNFLOW_ADMIN_EMAIL - cornflow_admin@cornflow.com
-    CORNFLOW_ADMIN_PWD - cornflowadmin1234
+    CORNFLOW_ADMIN_PWD - Cornflowadmin_1234
 
 It is advisable to change the default admin user and keep the password in a safe place.
 
@@ -199,4 +199,4 @@ For example, here are some example values for each of the ldap_user_type object:
         (objectClass=posixAccount) for RFC-2037 and RFC-2037bis
         (objectClass=sambaSamAccount) for SAMBA 3.0.x LDAP extension
         (objectClass=user) for MS-AD
-        (objectClass=*) for Default
\ No newline at end of file
+        (objectClass=*) for Default
diff --git a/docs/source/deploy/docker-compose.yml.rst b/docs/source/deploy/docker-compose.yml.rst
index a4f68add..57ab618f 100644
--- a/docs/source/deploy/docker-compose.yml.rst
+++ b/docs/source/deploy/docker-compose.yml.rst
@@ -38,3 +38,4 @@ Stop and clean docker environment
 Stop the docker services and remove all volumes::
 
     docker-compose down --volumes --rmi all
+
diff --git a/docs/source/deploy/docker-stack.rst b/docs/source/deploy/docker-stack.rst
index e00793de..f8219447 100644
--- a/docs/source/deploy/docker-stack.rst
+++ b/docs/source/deploy/docker-stack.rst
@@ -92,3 +92,4 @@ The postgresql environment variables included in ``docker-compose.yml`` are::
 The postgresql deployment requires mounting one volume linked to the directory created on the host::
 
     postgres_cf_data:/var/lib/postgresql/data/ - This volume stores the database files
+
diff --git a/docs/source/deploy/options.rst b/docs/source/deploy/options.rst
index d7b2c99b..38e093c0 100644
--- a/docs/source/deploy/options.rst
+++ b/docs/source/deploy/options.rst
@@ -43,7 +43,7 @@ To create a database, you must be a superuser. A user called postgres is made on
 
 Before deploying Cornflow, set the environment variable with the address of the database::
 
-    docker run -e DATABASE_URL=postgres://myuser:myuserpwd@myserverip:myserverport/cornflow -d --name=cornflow baobabsoluciones/cornflow
+    docker run -e DATABASE_URL=postgresql://myuser:myuserpwd@myserverip:myserverport/cornflow -d --name=cornflow baobabsoluciones/cornflow
     
 Connect to your own airflow deployment
 *******************************************
@@ -74,4 +74,4 @@ If you want to install packages that require a secure connection to a server, yo
 
 When you have included the key file in the airflow service, you must enter the server with which it will be used by means of an environment variable. For instance::
 
-    CUSTOM_SSH_HOST=github.com
\ No newline at end of file
+    CUSTOM_SSH_HOST=github.com
diff --git a/docs/source/deploy/problems.rst b/docs/source/deploy/problems.rst
index 632babca..db784e6a 100644
--- a/docs/source/deploy/problems.rst
+++ b/docs/source/deploy/problems.rst
@@ -50,9 +50,9 @@ Running cornflow-server
 
 Problem: Users were not created when cornflow started
 
-    Error: usage: manage.py [-?] {db,create_admin_user,create_service_user,access_init,register_base_assignations,register_actions,register_views,register_roles,update_views,clean_historic_data,shell,runserver} ...
+    Error: usage: flask [-?] {db,create_admin_user,create_service_user,access_init,register_base_assignations,register_actions,register_views,register_roles,update_views,clean_historic_data,shell,runserver} ...
 
-    Possible solution: If you have modified the entrypoint script, checks that the conditions are met in the execution of python mange.py create_admin_user/create_service_user.
+    Possible solution: If you have modified the entrypoint script, checks that the conditions are met in the execution of flask create_admin_user/create_service_user.
 
 Flower 
 ^^^^^^^^^^
diff --git a/docs/source/deploy/production.rst b/docs/source/deploy/production.rst
index 54537d32..f40ebde4 100644
--- a/docs/source/deploy/production.rst
+++ b/docs/source/deploy/production.rst
@@ -9,7 +9,7 @@ Database backend
 Running the default docker-compose setup in production can lead to data loss in multiple scenarios. If you want to run production-grade Cornflow, make sure you configure the backend to be an external PostgreSQL.
 You can change the backend using the following config::
 
-    DATABASE_URL=postgres://myuser:myuserpwd@myserverip:5432/cornflow
+    DATABASE_URL=postgresql://myuser:myuserpwd@myserverip:5432/cornflow
 
 SSL
 ******
diff --git a/docs/source/deploy/simultaneous.rst b/docs/source/deploy/simultaneous.rst
index b66c57e5..050eaff1 100644
--- a/docs/source/deploy/simultaneous.rst
+++ b/docs/source/deploy/simultaneous.rst
@@ -65,4 +65,4 @@ Flower service available at http://localhost:5555
 
 If you want to stop the docker services and remove all volumes::
 
-    docker-compose down -f docker-compose-cornflow-celery.yml --volumes --rmi all
\ No newline at end of file
+    docker-compose down -f docker-compose-cornflow-celery.yml --volumes --rmi all
diff --git a/docs/source/dev/endpoints.rst b/docs/source/dev/endpoints.rst
index 14bf98b6..b2828d46 100644
--- a/docs/source/dev/endpoints.rst
+++ b/docs/source/dev/endpoints.rst
@@ -1,6 +1,15 @@
 Endpoints
 ==========
 
+Case endpoints
+---------------
+
+.. automodule:: cornflow.endpoints.case
+    :members:
+    :undoc-members:
+    :show-inheritance:
+    :member-order: bysource
+
 DAG endpoint
 ----------------
 
diff --git a/docs/source/dev/models.rst b/docs/source/dev/models.rst
index 4c684933..44a73e67 100644
--- a/docs/source/dev/models.rst
+++ b/docs/source/dev/models.rst
@@ -1,6 +1,14 @@
 Models
 ==========================
 
+Case model
+-----------
+
+.. automodule:: cornflow.models.case
+   :members:
+   :show-inheritance:
+   :member-order: bysource
+
 Execution model
 ------------------
 
diff --git a/docs/source/main/install.rst b/docs/source/main/install.rst
index b3460d1c..57c6d6f9 100644
--- a/docs/source/main/install.rst
+++ b/docs/source/main/install.rst
@@ -16,16 +16,16 @@ Download the Cornflow project::
 
     git clone git@github.com:baobabsoluciones/cornflow-server.git
     cd corn
-    python3 -m venv cfvenv
-    cfvenv/bin/pip3 install -r requirements-dev.txt
+    python3 -m venv venv
+    venv/bin/pip3 install -r requirements-dev.txt
 
 activate the virtual environment::
 
-    source cfvenv/bin/activate
+    source venv/bin/activate
 
 or, in windows::
 
-    cfvenv/Scripts/activate
+    venv/Scripts/activate
 
 
 Setup cornflow database
@@ -35,20 +35,20 @@ This local example assumes sqlite as database engine. For an example of postgres
 
 Initialize the database::
 
-    source cfvenv/bin/activate
+    source venv/bin/activate
     export FLASK_APP=cornflow.app
     export DATABASE_URL=sqlite:///cornflow.db
-    python manage.py db upgrade
-    python manage.py access_init
-    python manage.py create_service_user  --username=airflow --email=airflow_test@admin.com --password=airflow_test_password
-    python manage.py create_admin_user  --username=cornflow --email=cornflow_admin@admin.com --password=cornflow_admin_password
+    flask db upgrade
+    flask access_init -v 0
+    flask create_service_user  -u airflow -e airflow_test@admin.com -p airflow_test_password -v 0
+    flask create_admin_user  -u cornflow -e cornflow_admin@admin.com -p cornflow_admin_password -v 0
 
 Launch cornflow server
 ~~~~~~~~~~~~~~~~~~~~~~~
 
 Each time you run the flask server, execute the following::
 
-    source cfvenv/bin/activate
+    source venv/bin/activate
     export FLASK_APP=cornflow.app
     export FLASK_ENV=development
     export DATABASE_URL=sqlite:///cornflow.db
diff --git a/examples/basic_functions.py b/examples/basic_functions.py
index d819f655..ef74b55f 100644
--- a/examples/basic_functions.py
+++ b/examples/basic_functions.py
@@ -1,8 +1,9 @@
 from cornflow_client import CornFlow
 
-email = 'some_email@gmail.com'
-pwd = 'some_password'
-name = 'some_name'
+email = "some_email@gmail.com"
+pwd = "some_password"
+name = "some_name"
+
 
 def run_example():
     server = "http://127.0.0.1:5000"
@@ -12,8 +13,8 @@ def run_example():
     a = client.sign_up(**config)
     a = client.login(email, pwd)
 
-
     import pulp
+
     prob = pulp.LpProblem("test_export_dict_MIP", pulp.LpMinimize)
     x = pulp.LpVariable("x", 0, 4)
     y = pulp.LpVariable("y", -1, 1)
@@ -23,71 +24,82 @@ def run_example():
     prob += x + z >= 10, "c2"
     prob += -y + z == 7.5, "c3"
     data = prob.toDict()
-    filename = 'test_mps.mps'
-    insName = 'test_export_dict_MIP'
-    description = 'very small example'
+    filename = "test_mps.mps"
+    insName = "test_export_dict_MIP"
+    description = "very small example"
 
     instance = client.create_instance(data, name=insName, description=description)
     # alternatively: send file
     prob.writeMPS(filename=filename)
-    instance = client.create_instance_file(filename=filename, name=insName, description=description)
+    instance = client.create_instance_file(
+        filename=filename, name=insName, description=description
+    )
 
     # edit the instance to give it a new name
-    client.put_api_for_id('instance/', instance['id'], dict(name='newName'))
+    client.put_api_for_id("instance/", instance["id"], dict(name="newName"))
 
     # get info from an instance
-    info = client.get_one_instance(instance['id'])
+    info = client.get_one_instance(instance["id"])
     # get all instances
     info_all = client.get_all_instances()
 
     # send an execution
     config = dict(solver="PULP_CBC_CMD", timeLimit=10)
     execution = client.create_execution(
-        instance['id'], config, name='execution1', description='execution of a very small instance'
+        instance["id"],
+        config,
+        name="execution1",
+        description="execution of a very small instance",
     )
 
     # check the status of the execution
-    status = client.get_status(execution['id'])
-    print(status['state'])
+    status = client.get_status(execution["id"])
+    print(status["state"])
     # get the execution solution
-    results = client.get_solution(execution['id'])
-    _vars, prob = pulp.LpProblem.from_dict(results['data'])
+    results = client.get_solution(execution["id"])
+    _vars, prob = pulp.LpProblem.from_dict(results["data"])
 
     # get the values for the variables:
     print({k: v.value() for k, v in _vars.items()})
 
     # get the log in json format
-    log = client.get_log(execution['id'])
-    print(log['log'])
+    log = client.get_log(execution["id"])
+    print(log["log"])
     # a json version of the log
 
 
 def example_migrate_execution():
-    email = 'airflow_test@admin.com'
-    pwd = 'airflow_test_password'
+    email = "airflow_test@admin.com"
+    pwd = "airflow_test_password"
     server = "http://127.0.0.1:5000"
     client = CornFlow(url=server)
     a = client.login(email, pwd)
     info_all = client.get_all_instances()
     instance = info_all[-1]
-    info = client.get_one_instance(instance['id'])
-    inst_data = client.get_api_for_id('instance/', instance['id'], 'data')
+    info = client.get_one_instance(instance["id"])
+    inst_data = client.get_api_for_id("instance/", instance["id"], "data")
     inst_data = inst_data.json()
-    execution = info['executions'][-1]
-    results = client.get_solution(execution['id'])
-    log = client.get_log(execution['id'])
+    execution = info["executions"][-1]
+    results = client.get_solution(execution["id"])
+    log = client.get_log(execution["id"])
 
     # now we go to the other server:
     server2 = "SOME_OTHER_SERVER"
     client2 = CornFlow(url=server2)
-    client2.login('ADMIN_USER', 'ADMIN_PASS')
+    client2.login("ADMIN_USER", "ADMIN_PASS")
 
     # create instance in another server
-    instance2 = client2.create_instance(inst_data['data'], name=info['name'], description=info['description'])
-    execution_new = client.manual_execution(instance_id=instance2['id'], config=execution['config'],
-                            name=execution['name'], data=results['data'],
-                            log_json=log['log'])
+    instance2 = client2.create_instance(
+        inst_data["data"], name=info["name"], description=info["description"]
+    )
+    execution_new = client.manual_execution(
+        instance_id=instance2["id"],
+        config=execution["config"],
+        name=execution["name"],
+        data=results["data"],
+        log_json=log["log"],
+    )
 
 
-if __name__ == '__main__':
+if __name__ == "__main__":
     run_example()
diff --git a/examples/pulp_varnames.py b/examples/pulp_varnames.py
index 03311345..975fb0fb 100644
--- a/examples/pulp_varnames.py
+++ b/examples/pulp_varnames.py
@@ -1,9 +1,10 @@
 from cornflow_client import CornFlow, group_variables_by_name
 import pulp
 
-email = 'some_email@gmail.com'
-pwd = 'some_password'
-name = 'some_name'
+email = "some_email@gmail.com"
+pwd = "some_password"
+name = "some_name"
+
 
 def run_example():
 
@@ -31,18 +32,20 @@ def run_example():
         gapRel=0.1,
         gapAbs=1,
         threads=1,
-        logPath="test_export_solver_json.log"
+        logPath="test_export_solver_json.log",
     )
 
     execution_id = client.create_execution(instance_id, config)
     status = client.get_status(execution_id)
     results = client.get_solution(execution_id)
 
-    _vars, prob = pulp.LpProblem.from_dict(results['data'])
-    actual_vars = group_variables_by_name(_vars, ['Route', 'BuildaPlant'], replace_underscores_with_spaces=True)
+    _vars, prob = pulp.LpProblem.from_dict(results["data"])
+    actual_vars = group_variables_by_name(
+        _vars, ["Route", "BuildaPlant"], replace_underscores_with_spaces=True
+    )
     actual_vars.keys()
-    actual_vars['BuildaPlant']
-    actual_vars['Route'][('San Francisco', 'Barstow')]
+    actual_vars["BuildaPlant"]
+    actual_vars["Route"][("San Francisco", "Barstow")]
 
     # The status of the solution is printed to the screen
     print("Status:", pulp.LpStatus[prob.status])
@@ -58,18 +61,15 @@ def run_example():
     {k: v.value() for k, v in _vars.items()}
 
     # get the log in text format
-    results['log_text']
+    results["log_text"]
 
     # get the log in json format
-    results['log_json']
+    results["log_json"]
 
 
 def build_pulp_problem():
     # Creates a list of all the supply nodes
-    Plants = ["San Francisco",
-              "Los Angeles",
-              "Phoenix",
-              "Denver"]
+    Plants = ["San Francisco", "Los Angeles", "Phoenix", "Denver"]
 
     # Creates a dictionary of lists for the number of units of supply at
     # each plant and the fixed cost of running each plant
@@ -77,21 +77,18 @@ def build_pulp_problem():
         "San Francisco": [1700, 70000],
         "Los Angeles": [2000, 70000],
         "Phoenix": [1700, 65000],
-        "Denver": [2000, 70000]
+        "Denver": [2000, 70000],
     }
 
     # Creates a list of all demand nodes
-    Stores = ["San Diego",
-              "Barstow",
-              "Tucson",
-              "Dallas"]
+    Stores = ["San Diego", "Barstow", "Tucson", "Dallas"]
 
     # Creates a dictionary for the number of units of demand at each store
     demand = {  # Store    Demand
         "San Diego": 1700,
         "Barstow": 1000,
         "Tucson": 1500,
-        "Dallas": 1200
+        "Dallas": 1200,
     }
 
     # Creates a list of costs for each transportation path
@@ -100,7 +97,7 @@ def build_pulp_problem():
         [5, 3, 2, 6],  # SF
         [4, 7, 8, 10],  # LA    Plants
         [6, 5, 3, 8],  # PH
-        [9, 8, 6, 5]  # DE
+        [9, 8, 6, 5],  # DE
     ]
 
     # Creates a list of tuples containing all the possible routes for transport
@@ -124,15 +121,24 @@ def build_pulp_problem():
     prob = pulp.LpProblem("Computer Plant Problem", pulp.LpMinimize)
 
     # The objective function is added to prob - The sum of the transportation costs and the building fixed costs
-    prob += pulp.lpSum([flow[p, s] * costs[p][s] for (p, s) in Routes]) + pulp.lpSum(
-        [fixedCost[p] * build[p] for p in Plants]), "Total Costs"
+    prob += (
+        pulp.lpSum([flow[p, s] * costs[p][s] for (p, s) in Routes])
+        + pulp.lpSum([fixedCost[p] * build[p] for p in Plants]),
+        "Total Costs",
+    )
 
     # The Supply maximum constraints are added for each supply node (plant)
     for p in Plants:
-        prob += pulp.lpSum([flow[p, s] for s in Stores]) <= supply[p] * build[p], "Sum of Products out of Plant %s" % p
+        prob += (
+            pulp.lpSum([flow[p, s] for s in Stores]) <= supply[p] * build[p],
+            "Sum of Products out of Plant %s" % p,
+        )
 
     # The Demand minimum constraints are added for each demand node (store)
     for s in Stores:
-        prob += pulp.lpSum([flow[p, s] for p in Plants]) >= demand[s], "Sum of Products into Stores %s" % s
+        prob += (
+            pulp.lpSum([flow[p, s] for p in Plants]) >= demand[s],
+            "Sum of Products into Stores %s" % s,
+        )
 
     return prob
diff --git a/init_cornflow_service.py b/init_cornflow_service.py
index 00c878eb..e475ed2e 100644
--- a/init_cornflow_service.py
+++ b/init_cornflow_service.py
@@ -7,7 +7,11 @@
 from flask_migrate import Migrate, upgrade
 from cornflow.shared.const import ADMIN_ROLE, AUTH_DB, SERVICE_ROLE
 from cornflow.app import create_app, db
-from cornflow.commands.commands import AccessInitialization, create_user_with_role
+from cornflow.commands.access import access_init_command
+from cornflow.commands.dag import register_deployed_dags_command
+from cornflow.commands.permissions import register_dag_permissions_command
+from cornflow.commands.schemas import update_schemas_command
+from cornflow.commands.users import create_user_with_role
 
 os.chdir("/usr/src/app")
 ENV = os.getenv("FLASK_ENV", "development")
@@ -33,24 +37,30 @@
 CORNFLOW_DB_USER = os.getenv("CORNFLOW_DB_USER", "cornflow")
 CORNFLOW_DB_PASSWORD = os.getenv("CORNFLOW_DB_PASSWORD", "cornflow")
 CORNFLOW_DB = os.getenv("CORNFLOW_DB", "cornflow")
-CORNFLOW_DB_CONN = os.getenv("CORNFLOW_DB_CONN",f"postgres://{CORNFLOW_DB_USER}:{CORNFLOW_DB_PASSWORD}@{CORNFLOW_DB_HOST}:{CORNFLOW_DB_PORT}/{CORNFLOW_DB}")
+CORNFLOW_DB_CONN = os.getenv(
+    "CORNFLOW_DB_CONN",
+    f"postgresql://{CORNFLOW_DB_USER}:{CORNFLOW_DB_PASSWORD}@{CORNFLOW_DB_HOST}:{CORNFLOW_DB_PORT}/{CORNFLOW_DB}",
+)
 os.environ["DATABASE_URL"] = CORNFLOW_DB_CONN
 
 # Platform auth config and service users
 AUTH = int(os.getenv("AUTH_TYPE", AUTH_DB))
 CORNFLOW_ADMIN_USER = os.getenv("CORNFLOW_ADMIN_USER", "cornflow_admin")
 CORNFLOW_ADMIN_EMAIL = os.getenv("CORNFLOW_ADMIN_EMAIL", "cornflow_admin@cornflow.com")
-CORNFLOW_ADMIN_PWD = os.getenv("CORNFLOW_ADMIN_PWD", "cornflowadmin1234")
+CORNFLOW_ADMIN_PWD = os.getenv("CORNFLOW_ADMIN_PWD", "Cornflowadmin_1234")
 CORNFLOW_SERVICE_USER = os.getenv("CORNFLOW_SERVICE_USER", "service_user")
 CORNFLOW_SERVICE_EMAIL = os.getenv(
     "CORNFLOW_SERVICE_EMAIL", "service_user@cornflow.com"
 )
-CORNFLOW_SERVICE_PWD = os.getenv("CORNFLOW_SERVICE_PWD", "serviceuser1234")
+CORNFLOW_SERVICE_PWD = os.getenv("CORNFLOW_SERVICE_PWD", "Serviceuser_1234")
 
 # Cornflow logging and storage config
 CORNFLOW_LOGGING = os.getenv("CORNFLOW_LOGGING", "console")
 os.environ["CORNFLOW_LOGGING"] = CORNFLOW_LOGGING
 
+OPEN_DEPLOYMENT = os.getenv("OPEN_DEPLOYMENT", 1)
+os.environ["OPEN_DEPLOYMENT"] = str(OPEN_DEPLOYMENT)
+
 # Check LDAP parameters for active directory and show message
 if os.getenv("AUTH_TYPE") == 2:
     print(
@@ -78,7 +88,7 @@
         out_logrotate = logrotate.stdout
         print(out_logrotate)
 
-    except (error):
+    except error:
         print(error)
 
 # make initdb, access control and/or migrations
@@ -86,7 +96,7 @@
 with app.app_context():
     migrate = Migrate(app=app, db=db)
     upgrade()
-    AccessInitialization.run(self=AccessInitialization, verbose=1)
+    access_init_command(0)
     # create user if auth type is db
     if AUTH == 1:
         # create cornflow admin user
@@ -107,6 +117,9 @@
             SERVICE_ROLE,
             verbose=1,
         )
+    register_deployed_dags_command(AIRFLOW_URL, AIRFLOW_USER, AIRFLOW_PWD, 1)
+    register_dag_permissions_command(OPEN_DEPLOYMENT, 1)
+    update_schemas_command(AIRFLOW_URL, AIRFLOW_USER, AIRFLOW_PWD, 1)
 
 # execute gunicorn application
 os.system(
diff --git a/manage.py b/manage.py
deleted file mode 100644
index bad41223..00000000
--- a/manage.py
+++ /dev/null
@@ -1,32 +0,0 @@
-from flask_script import Manager
-from flask_migrate import Migrate, MigrateCommand
-from cornflow.commands import *
-
-from cornflow.app import create_app, db
-
-env_name = "development"
-app = create_app(env_name)
-
-migrate = Migrate(app=app, db=db)
-manager = Manager(app=app)
-
-# Database commands
-manager.add_command("db", MigrateCommand)
-
-# User commands
-manager.add_command("create_admin_user", CreateAdminUser)
-manager.add_command("create_service_user", CreateServiceUser)
-
-# Access control commands
-manager.add_command("access_init", AccessInitialization)
-manager.add_command("register_base_assignations", RegisterBasePermissions)
-manager.add_command("register_actions", RegisterActions)
-manager.add_command("register_views", RegisterViews)
-manager.add_command("register_roles", RegisterRoles)
-
-# Other commands
-manager.add_command("clean_historic_data", CleanHistoricData)
-
-
-if __name__ == "__main__":
-    manager.run()
diff --git a/migrations/README b/migrations/README
index 98e4f9c4..0e048441 100644
--- a/migrations/README
+++ b/migrations/README
@@ -1,1 +1,1 @@
-Generic single-database configuration.
\ No newline at end of file
+Single-database configuration for Flask.
diff --git a/migrations/alembic.ini b/migrations/alembic.ini
index f8ed4801..ec9d45c2 100644
--- a/migrations/alembic.ini
+++ b/migrations/alembic.ini
@@ -11,7 +11,7 @@
 
 # Logging configuration
 [loggers]
-keys = root,sqlalchemy,alembic
+keys = root,sqlalchemy,alembic,flask_migrate
 
 [handlers]
 keys = console
@@ -34,6 +34,11 @@ level = INFO
 handlers =
 qualname = alembic
 
+[logger_flask_migrate]
+level = INFO
+handlers =
+qualname = flask_migrate
+
 [handler_console]
 class = StreamHandler
 args = (sys.stderr,)
diff --git a/migrations/env.py b/migrations/env.py
index 94521792..68feded2 100644
--- a/migrations/env.py
+++ b/migrations/env.py
@@ -3,8 +3,7 @@
 import logging
 from logging.config import fileConfig
 
-from sqlalchemy import engine_from_config
-from sqlalchemy import pool
+from flask import current_app
 
 from alembic import context
 
@@ -21,10 +20,10 @@
 # for 'autogenerate' support
 # from myapp import mymodel
 # target_metadata = mymodel.Base.metadata
-from flask import current_app
 config.set_main_option(
     'sqlalchemy.url',
-    str(current_app.extensions['migrate'].db.engine.url).replace('%', '%%'))
+    str(current_app.extensions['migrate'].db.get_engine().url).replace(
+        '%', '%%'))
 target_metadata = current_app.extensions['migrate'].db.metadata
 
 # other values from the config, defined by the needs of env.py,
@@ -72,11 +71,7 @@ def process_revision_directives(context, revision, directives):
                 directives[:] = []
                 logger.info('No changes in schema detected.')
 
-    connectable = engine_from_config(
-        config.get_section(config.config_ini_section),
-        prefix='sqlalchemy.',
-        poolclass=pool.NullPool,
-    )
+    connectable = current_app.extensions['migrate'].db.get_engine()
 
     with connectable.connect() as connection:
         context.configure(
diff --git a/migrations/versions/a472b5ad50b7_.py b/migrations/versions/a472b5ad50b7_.py
index aa9607b6..f7151db4 100644
--- a/migrations/versions/a472b5ad50b7_.py
+++ b/migrations/versions/a472b5ad50b7_.py
@@ -9,7 +9,7 @@
 import sqlalchemy as sa
 from alembic import op
 
-from cornflow.shared.const import DEFAULT_EXECUTION_CODE
+# TODO: import DEFAULT EXECUTION CODE HERE
 
 # revision identifiers, used by Alembic.
 revision = "a472b5ad50b7"
@@ -26,7 +26,7 @@ def upgrade():
             "state",
             sa.SmallInteger(),
             nullable=False,
-            server_default=sa.text(str(DEFAULT_EXECUTION_CODE)),
+            server_default=sa.text(str(0)),
         ),
     )
 
diff --git a/migrations/versions/c8a6c762e818_.py b/migrations/versions/c8a6c762e818_.py
index 86fa806d..32dcc5b1 100644
--- a/migrations/versions/c8a6c762e818_.py
+++ b/migrations/versions/c8a6c762e818_.py
@@ -52,7 +52,7 @@ def upgrade():
         sa.Column("deleted_at", sa.DateTime(), nullable=True),
         sa.Column("id", sa.Integer(), nullable=False),
         sa.Column("action_id", sa.Integer(), nullable=False),
-        sa.Column("api_view_id", sa.Integer(), nullable=True),
+        sa.Column("api_view_id", sa.Integer(), nullable=False),
         sa.Column("role_id", sa.Integer(), nullable=False),
         sa.ForeignKeyConstraint(
             ["action_id"],
diff --git a/migrations/versions/f3bee20314a2_.py b/migrations/versions/f3bee20314a2_.py
new file mode 100644
index 00000000..d729d478
--- /dev/null
+++ b/migrations/versions/f3bee20314a2_.py
@@ -0,0 +1,58 @@
+"""
+Added DAG master table and DAG permissions table
+
+Revision ID: f3bee20314a2
+Revises: ca449af8034c
+Create Date: 2021-12-14 14:41:16.096297
+
+"""
+from alembic import op
+import sqlalchemy as sa
+
+
+# revision identifiers, used by Alembic.
+revision = "f3bee20314a2"
+down_revision = "ca449af8034c"
+branch_labels = None
+depends_on = None
+
+
+def upgrade():
+    # ### commands auto generated by Alembic - please adjust! ###
+    op.create_table(
+        "deployed_dags",
+        sa.Column("created_at", sa.DateTime(), nullable=False),
+        sa.Column("updated_at", sa.DateTime(), nullable=False),
+        sa.Column("deleted_at", sa.DateTime(), nullable=True),
+        sa.Column("id", sa.String(length=128), nullable=False),
+        sa.Column("description", sa.TEXT(), nullable=True),
+        sa.PrimaryKeyConstraint("id"),
+    )
+
+    op.create_table(
+        "permission_dag",
+        sa.Column("created_at", sa.DateTime(), nullable=False),
+        sa.Column("updated_at", sa.DateTime(), nullable=False),
+        sa.Column("deleted_at", sa.DateTime(), nullable=True),
+        sa.Column("id", sa.Integer(), autoincrement=True, nullable=False),
+        sa.Column("dag_id", sa.String(length=128), nullable=False),
+        sa.Column("user_id", sa.Integer(), nullable=False),
+        sa.ForeignKeyConstraint(
+            ["dag_id"],
+            ["deployed_dags.id"],
+        ),
+        sa.ForeignKeyConstraint(
+            ["user_id"],
+            ["users.id"],
+        ),
+        sa.PrimaryKeyConstraint("id"),
+        sa.UniqueConstraint("dag_id", "user_id"),
+    )
+    # ### end Alembic commands ###
+
+
+def downgrade():
+    # ### commands auto generated by Alembic - please adjust! ###
+    op.drop_table("permission_dag")
+    op.drop_table("deployed_dags")
+    # ### end Alembic commands ###
diff --git a/requirements-dev.txt b/requirements-dev.txt
index aa2ee0bc..033a792c 100644
--- a/requirements-dev.txt
+++ b/requirements-dev.txt
@@ -1,23 +1,23 @@
 -r requirements.txt
-alabaster==0.7.12
-Babel==2.9.1
-colorama==0.4.4
-coverage==5.3
-docutils==0.16
-Flask-Testing==0.8.0
-imagesize==1.2.0
+alabaster
+Babel
+colorama
+coverage
+docutils
+Flask-Testing
+imagesize
 orloge
-packaging==20.4
-Pygments==2.7.4
-pyparsing==2.4.7
-snowballstemmer==2.0.0
-Sphinx==3.2.1
+packaging
+Pygments
+pyparsing
+snowballstemmer
+Sphinx
 sphinx_rtd_theme
-sphinxcontrib-applehelp==1.0.2
-sphinxcontrib-devhelp==1.0.2
-sphinxcontrib-htmlhelp==1.0.3
-sphinxcontrib-httpdomain==1.7.0
-sphinxcontrib-jsmath==1.0.1
-sphinxcontrib-qthelp==1.0.3
-sphinxcontrib-serializinghtml==1.1.4
+sphinxcontrib-applehelp
+sphinxcontrib-devhelp
+sphinxcontrib-htmlhelp
+sphinxcontrib-httpdomain
+sphinxcontrib-jsmath
+sphinxcontrib-qthelp
+sphinxcontrib-serializinghtml
 sphinxcontrib-redoc
diff --git a/requirements.txt b/requirements.txt
index 5dfd3527..726767b9 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,45 +1,45 @@
-alembic==1.4.2
-aniso8601==8.0.0
+alembic
+aniso8601
 apispec
-bcrypt==3.1.7
-certifi==2020.6.20
-cffi==1.14.0
-chardet==3.0.4
-click==7.1.2
+bcrypt
+certifi
+cffi
+chardet
+click
 cornflow-client @ git+https://github.com/baobabsoluciones/cornflow-client@master
-cryptography>=3.3.2
-Flask==1.1.2
+cryptography
+Flask
 flask-apispec
-Flask-Bcrypt==0.7.1
-flask-compress==1.9.0
-flask-cors==3.0.10
-Flask-Migrate==2.5.3
-flask-inflate==0.3
-Flask-RESTful==0.3.8
-Flask-Script==2.0.6
-Flask-SQLAlchemy==2.4.1
-gevent==1.4.0
-greenlet==0.4.15
-gunicorn==20.0.4
-idna==2.10
-itsdangerous==1.1.0
-Jinja2==2.11.3
-jsonpatch==1.32
-ldap3==2.9
-Mako==1.1.2
-MarkupSafe==1.1.1
+Flask-Bcrypt
+flask-compress
+flask-cors
+Flask-Migrate
+flask-inflate
+Flask-RESTful
+Flask-SQLAlchemy
+gevent
+greenlet
+gunicorn
+idna
+itsdangerous
+Jinja2
+jsonpatch
+ldap3
+Mako
+MarkupSafe
 marshmallow
-psycopg2==2.8.5
+psycopg2
 PuLP
-pyasn1==0.4.8
-pycparser==2.20
-PyJWT==1.7.1
-python-dateutil==2.8.1
-python-editor==1.0.4
-pytz==2020.1
-requests==2.25.0
-six==1.14.0
-SQLAlchemy==1.3.17
-urllib3==1.26.5
-webargs==8.0.0
-Werkzeug==1.0.1
+pyasn1
+pycparser
+PyJWT
+python-dateutil
+python-editor
+pytz
+requests
+six
+# waiting on new version of flask-sqlalchemy to upgrade sqlalchemy version
+SQLAlchemy==1.3.21
+urllib3
+webargs
+Werkzeug


Test patch:
diff --git a/.github/workflows/run_tests.yml b/.github/workflows/run_tests.yml
index 308c1744..0431fad6 100644
--- a/.github/workflows/run_tests.yml
+++ b/.github/workflows/run_tests.yml
@@ -28,3 +28,4 @@ jobs:
       run: python -m unittest discover -s cornflow.tests.unit
       env:
         FLASK_ENV: testing
+        DATABASE_URL: sqlite:///cornflow_test.db
diff --git a/.github/workflows/run_tests_airflow.yml b/.github/workflows/run_tests_airflow.yml
index 23aa0863..dd1f3963 100644
--- a/.github/workflows/run_tests_airflow.yml
+++ b/.github/workflows/run_tests_airflow.yml
@@ -28,7 +28,7 @@ jobs:
       run: |
         python -m venv afvenv
         source afvenv/bin/activate
-        AIRFLOW_VERSION=2.1.0
+        AIRFLOW_VERSION=2.2.1
         PYTHON_VERSION="$(python3 --version | cut -d " " -f 2 | cut -d "." -f 1-2)"
         CONSTRAINT_URL="https://raw.githubusercontent.com/apache/airflow/constraints-${AIRFLOW_VERSION}/constraints-${PYTHON_VERSION}.txt"
         python -m pip install "apache-airflow==${AIRFLOW_VERSION}" --constraint "${CONSTRAINT_URL}"
@@ -53,8 +53,9 @@ jobs:
         AIRFLOW__API__AUTH_BACKEND: airflow.api.auth.backend.basic_auth
         AIRFLOW__WEBSERVER__SECRET_KEY: e9adafa751fd35adfc1fdd3285019be15eea0758f76e38e1e37a1154fb36
         AIRFLOW__CORE__LOAD_EXAMPLES: 0
-        AIRFLOW_CONN_CF_URI: http://airflow:airflow_test_password@localhost:5000
+        AIRFLOW_CONN_CF_URI: http://airflow:Airflow_test_password1@localhost:5050
     - name: Run unit tests
       run: python -m unittest discover -s cornflow.tests.integration
       env:
         FLASK_ENV: testing
+        DATABASE_URL: sqlite:///cornflow_test.db
diff --git a/.github/workflows/run_tests_ldap.yml b/.github/workflows/run_tests_ldap.yml
index 6f2419b0..99f44c4f 100644
--- a/.github/workflows/run_tests_ldap.yml
+++ b/.github/workflows/run_tests_ldap.yml
@@ -32,6 +32,7 @@ jobs:
       run: python -m unittest discover -s cornflow.tests.ldap
       env:
         FLASK_ENV: testing
+        DATABASE_URL: sqlite:///cornflow_test.db
         AUTH_TYPE: 2
         LDAP_HOST: ldap://localhost:389
         LDAP_BIND_PASSWORD: admin
@@ -45,7 +46,7 @@ jobs:
         LDAP_EMAIL_ATTRIBUTE: mail
         LDAP_USER_OBJECT_CLASS: inetOrgPerson
         LDAP_GROUP_ATTRIBUTE: cn
-        LDAP_GROUP_TO_ROLE_SERVICE: service
+        LDAP_GROUP_TO_ROLE_SERVICE: services
         LDAP_GROUP_TO_ROLE_ADMIN: administrators
         LDAP_GROUP_TO_ROLE_VIEWER: viewers
         LDAP_GROUP_TO_ROLE_PLANNER: planners
diff --git a/.github/workflows/run_tests_postgresql.yml b/.github/workflows/run_tests_postgresql.yml
index df28ed3f..c74c6ef6 100644
--- a/.github/workflows/run_tests_postgresql.yml
+++ b/.github/workflows/run_tests_postgresql.yml
@@ -43,4 +43,4 @@ jobs:
       run: python -m unittest discover -s cornflow.tests.unit
       env:
         FLASK_ENV: testing
-        DATABASE_URL: postgres://postgres:postgresadmin@127.0.0.1:5432/cornflow-test
+        DATABASE_URL: postgresql://postgres:postgresadmin@127.0.0.1:5432/cornflow-test
diff --git a/cornflow/tests/const.py b/cornflow/tests/const.py
index 2aa5a349..21f340d5 100644
--- a/cornflow/tests/const.py
+++ b/cornflow/tests/const.py
@@ -51,3 +51,5 @@ def _get_file(relative_path):
 USER_ROLE_URL = PREFIX + "/user/role/"
 
 APIVIEW_URL = PREFIX + "/apiview/"
+
+DEPLOYED_DAG_URL = PREFIX + "/dag/deployed/"
diff --git a/cornflow/tests/custom_liveServer.py b/cornflow/tests/custom_liveServer.py
index 2ee3382d..cc1d464f 100644
--- a/cornflow/tests/custom_liveServer.py
+++ b/cornflow/tests/custom_liveServer.py
@@ -1,20 +1,25 @@
-from flask_testing import LiveServerTestCase
+# Full imports
 import cornflow_client as cf
-import json
 
+# External libraries
+from flask import current_app
+from flask_testing import LiveServerTestCase
+
+# Internal modules
 from cornflow.app import create_app
-from cornflow.commands import AccessInitialization
+from cornflow.models import UserRoleModel
+from cornflow.commands.access import access_init_command
+from cornflow.commands.dag import register_deployed_dags_command_test
+from cornflow.commands.permissions import register_dag_permissions_command
+from cornflow.shared.const import ADMIN_ROLE, SERVICE_ROLE
 from cornflow.shared.utils import db
 from cornflow.tests.const import PREFIX
 
-from cornflow.models import UserModel, UserRoleModel
-from cornflow.shared.const import ADMIN_ROLE, SERVICE_ROLE
-from cornflow.tests.const import LOGIN_URL, SIGNUP_URL
-
 
 class CustomTestCaseLive(LiveServerTestCase):
     def create_app(self):
         app = create_app("testing")
+        app.config["LIVESERVER_PORT"] = 5050
         return app
 
     def set_client(self, server):
@@ -31,11 +36,12 @@ def login_or_signup(self, user_data):
     def setUp(self, create_all=True):
         if create_all:
             db.create_all()
-        AccessInitialization().run()
+        access_init_command(0)
+        register_deployed_dags_command_test(verbose=0)
         user_data = dict(
             username="testname",
             email="test@test.com",
-            pwd="testpassword",
+            pwd="Testpassword1!",
         )
         self.set_client(self.get_server_url())
         response = self.login_or_signup(user_data)
@@ -43,6 +49,9 @@ def setUp(self, create_all=True):
         self.url = None
         self.model = None
         self.items_to_check = []
+        register_dag_permissions_command(
+            open_deployment=current_app.config["OPEN_DEPLOYMENT"], verbose=0
+        )
 
     def tearDown(self):
         db.session.remove()
@@ -53,7 +62,7 @@ def create_user_with_role(self, role_id, data=None):
             data = {
                 "username": "testuser" + str(role_id),
                 "email": "testemail" + str(role_id) + "@test.org",
-                "password": "testpassword",
+                "password": "Testpassword1!",
             }
         response = self.login_or_signup(data)
         user_role = UserRoleModel({"user_id": response["id"], "role_id": role_id})
diff --git a/cornflow/tests/custom_test_case.py b/cornflow/tests/custom_test_case.py
index f8f73ae0..0d7516a8 100644
--- a/cornflow/tests/custom_test_case.py
+++ b/cornflow/tests/custom_test_case.py
@@ -9,13 +9,13 @@
 import json
 import jwt
 from unittest.mock import patch, Mock
-import cProfile
-import pstats
 
 # Import from internal modules
 from cornflow.app import create_app
-from cornflow.commands import AccessInitialization
-from cornflow.models import UserModel, UserRoleModel
+from cornflow.models import UserRoleModel
+from cornflow.commands.access import access_init_command
+from cornflow.commands.dag import register_deployed_dags_command_test
+from cornflow.commands.permissions import register_dag_permissions_command
 from cornflow.shared.authentication import Auth
 from cornflow.shared.const import ADMIN_ROLE, PLANNER_ROLE, SERVICE_ROLE
 from cornflow.shared.utils import db
@@ -51,11 +51,12 @@ def load_file(_file, fk=None, fk_id=None):
 
     def setUp(self):
         db.create_all()
-        AccessInitialization().run(verbose=0)
+        access_init_command(0)
+        register_deployed_dags_command_test(verbose=0)
         data = {
             "username": "testname",
             "email": "test@test.com",
-            "password": "testpassword",
+            "password": "Testpassword1!",
         }
 
         self.client.post(
@@ -65,6 +66,10 @@ def setUp(self):
             headers={"Content-Type": "application/json"},
         )
 
+        register_dag_permissions_command(
+            open_deployment=int(current_app.config["OPEN_DEPLOYMENT"]), verbose=0
+        )
+
         data.pop("email")
 
         self.token = self.client.post(
@@ -93,7 +98,8 @@ def create_user(self, data):
             headers={"Content-Type": "application/json"},
         )
 
-    def create_role(self, user_id, role_id):
+    @staticmethod
+    def assign_role(user_id, role_id):
 
         if UserRoleModel.check_if_role_assigned(user_id, role_id):
             user_role = UserRoleModel.query.filter_by(
@@ -116,10 +122,10 @@ def create_user_with_role(self, role_id):
         data = {
             "username": "testuser" + str(role_id),
             "email": "testemail" + str(role_id) + "@test.org",
-            "password": "testpassword",
+            "password": "Testpassword1!",
         }
         response = self.create_user(data)
-        self.create_role(response.json["id"], role_id)
+        self.assign_role(response.json["id"], role_id)
 
         data.pop("email")
         return self.client.post(
@@ -153,6 +159,7 @@ def create_new_row(
             follow_redirects=True,
             headers=self.get_header_with_auth(token),
         )
+
         self.assertEqual(expected_status, response.status_code)
         if not check_payload:
             return response.json
@@ -437,7 +444,11 @@ def test_update_one_row(self):
                 self.url_with_query_arguments(), self.model, self.payload
             )
             payload = {**self.payload, **dict(id=idx, name="new_name")}
-            self.update_row(self.url + str(idx) + "/", dict(name="new_name"), payload)
+            self.update_row(
+                self.url + str(idx) + "/",
+                dict(name="new_name"),
+                payload,
+            )
 
         def test_update_one_row_bad_format(self):
             idx = self.create_new_row(
@@ -637,10 +648,10 @@ def test_old_token(self):
                 headers={"Content-Type": "application/json"},
             )
 
-            self.id = response.json["id"]
+            self.idx = response.json["id"]
 
             response = self.client.get(
-                USER_URL + str(self.id) + "/",
+                USER_URL + str(self.idx) + "/",
                 follow_redirects=True,
                 headers={
                     "Content-Type": "application/json",
@@ -662,10 +673,10 @@ def test_bad_format_token(self):
             )
 
             token = response.json["token"]
-            self.id = response.json["id"]
+            self.idx = response.json["id"]
 
             response = self.client.get(
-                USER_URL + str(self.id) + "/",
+                USER_URL + str(self.idx) + "/",
                 follow_redirects=True,
                 headers={
                     "Content-Type": "application/json",
@@ -688,10 +699,10 @@ def test_invalid_token(self):
                 follow_redirects=True,
                 headers={"Content-Type": "application/json"},
             )
-            self.id = response.json["id"]
+            self.idx = response.json["id"]
 
             response = self.client.get(
-                USER_URL + str(self.id) + "/",
+                USER_URL + str(self.idx) + "/",
                 follow_redirects=True,
                 headers={
                     "Content-Type": "application/json",
diff --git a/cornflow/tests/data/json_patch_bad.json b/cornflow/tests/data/json_patch_bad.json
index 91802ef8..c6a9ac4d 100644
--- a/cornflow/tests/data/json_patch_bad.json
+++ b/cornflow/tests/data/json_patch_bad.json
@@ -31,5 +31,6 @@
       "path": "/name",
       "value": "test_case_2"
     }
-  ]
+  ],
+  "schema": "solve_model_dag"
 }
\ No newline at end of file
diff --git a/cornflow/tests/data/new_instance_2.json b/cornflow/tests/data/new_instance_2.json
index 39567bdd..ff879a94 100644
--- a/cornflow/tests/data/new_instance_2.json
+++ b/cornflow/tests/data/new_instance_2.json
@@ -122,4 +122,5 @@
     "sos2": []
 },
 "name":"instance_2",
-"description":"This is a test instance to check if name and description get saved succesfully"}
\ No newline at end of file
+"description":"This is a test instance to check if name and description get saved succesfully",
+"schema":  "solve_model_dag"}
\ No newline at end of file
diff --git a/cornflow/tests/integration/test_commands.py b/cornflow/tests/integration/test_commands.py
new file mode 100644
index 00000000..84aeb032
--- /dev/null
+++ b/cornflow/tests/integration/test_commands.py
@@ -0,0 +1,18 @@
+from cornflow.tests.custom_liveServer import CustomTestCaseLive
+from flask import current_app
+from cornflow.models import DeployedDAG
+from cornflow.commands.dag import register_deployed_dags_command
+
+
+class TestCornflowCommands(CustomTestCaseLive):
+    def setUp(self, create_all=True):
+        super().setUp()
+
+    def test_dag_command(self):
+        config = current_app.config
+        register_deployed_dags_command(
+            config["AIRFLOW_URL"], config["AIRFLOW_USER"], config["AIRFLOW_PWD"], 0
+        )
+        dags = DeployedDAG.get_all_objects()
+        for dag in ["solve_model_dag", "gc", "timer"]:
+            self.assertIn(dag, [d.id for d in dags])
diff --git a/cornflow/tests/integration/test_cornflowclient.py b/cornflow/tests/integration/test_cornflowclient.py
index 4c60a739..7f530979 100644
--- a/cornflow/tests/integration/test_cornflowclient.py
+++ b/cornflow/tests/integration/test_cornflowclient.py
@@ -1,22 +1,29 @@
+"""
+
+"""
+# Full imports
 import json
 import pulp
 import logging as log
 import time
 import unittest
 
+# Imports from environment
 from cornflow_client import CornFlowApiError
 from cornflow_client.constants import INSTANCE_SCHEMA, SOLUTION_SCHEMA
 
-from cornflow.tests.custom_liveServer import CustomTestCaseLive
+# Import internal modules
+from airflow_config.dags.model_functions import solve as solve_model
+from cornflow.app import create_app
+from cornflow.schemas.solution_log import LogSchema
 from cornflow.shared.const import (
     EXEC_STATE_CORRECT,
     EXEC_STATE_STOPPED,
     EXEC_STATE_RUNNING,
+    STATUS_HEALTHY,
 )
 from cornflow.tests.const import INSTANCE_PATH
-from cornflow.shared.const import STATUS_HEALTHY
-from cornflow.schemas.solution_log import LogSchema
-from airflow_config.dags.model_functions import solve as solve_model
+from cornflow.tests.custom_liveServer import CustomTestCaseLive
 
 
 def load_file(_file):
@@ -56,7 +63,7 @@ def create_new_instance(self, mps_file):
         description = "description123"
         data = pulp.LpProblem.fromMPS(mps_file, sense=1)[1].toDict()
         schema = "solve_model_dag"
-        payload = dict(data=data, name=name, description=description)
+        payload = dict(data=data, name=name, description=description, schema=schema)
         return self.create_new_instance_payload(payload)
 
     def create_new_instance_payload(self, payload):
@@ -133,7 +140,7 @@ def create_timer_instance_and_execution(self, seconds=5):
         return self.create_new_execution(payload)
 
 
-class TestCornflowClient(TestCornflowClientBasic):
+class TestCornflowClientOpen(TestCornflowClientBasic):
 
     # TODO: user management
     # TODO: infeasible execution
@@ -144,10 +151,14 @@ def test_new_instance_file(self):
     def test_new_instance(self):
         return self.create_new_instance("./cornflow/tests/data/test_mps.mps")
 
-    def test_get_instance__data(self):
-        instance = self.create_new_instance("./cornflow/tests/data/test_mps.mps")
-        response = self.client.get_api_for_id("instance", instance["id"], "data")
-        self.assertEqual(response.headers["Content-Encoding"], "gzip")
+    # TODO: reactivate test with new version of cornflow client which allows to pass
+    #  optional arguments for the headers of the request
+    # def test_get_instance__data(self):
+    #     instance = self.create_new_instance("./cornflow/tests/data/test_mps.mps")
+    #     response = self.client.get_api_for_id(
+    #         "instance", instance["id"], "data", encoding="gzip"
+    #     )
+    #     self.assertEqual(response.headers["Content-Encoding"], "gzip")
 
     def test_delete_instance(self):
         instance = self.test_new_instance()
@@ -259,7 +270,22 @@ def test_server_alive(self):
         self.assertEqual(af_status, STATUS_HEALTHY)
 
 
-# TODO: maybe we should have a test-suite for service_user
+class TestCornflowClientNotOpen(TestCornflowClientBasic):
+    def create_app(self):
+        app = create_app("testing")
+        app.config["LIVESERVER_PORT"] = 5050
+        app.config["OPEN_DEPLOYMENT"] = 0
+        return app
+
+    def test_get_all_schemas(self):
+        response = self.client.get_all_schemas()
+        self.assertEqual([], response)
+
+    def test_get_one_schema(self):
+        response = self.client.get_schema("solve_model_dag")
+        self.assertEqual(
+            {"error": "User does not have permission to access this dag"}, response
+        )
 
 
 class TestCornflowClientAdmin(TestCornflowClientBasic):
@@ -268,15 +294,24 @@ def setUp(self, create_all=False):
 
         # we create a service user:
         self.create_service_user(
-            dict(username="airflow", pwd="airflow_test_password", email="af@cf.com")
+            dict(username="airflow", pwd="Airflow_test_password1", email="af@cf.com")
         )
+
+        self.create_service_user(
+            dict(
+                username="service_user@cornflow.com",
+                pwd="Serviceuser_1234",
+                email="service_user@cornflow.com",
+            )
+        )
+
         # we create an admin user
         # we guarantee that the admin is there for airflow
         self.client.token = self.create_admin(
             dict(
                 username="airflow_test@admin.com",
                 email="airflow_test@admin.com",
-                pwd="airflow_test_password",
+                pwd="Airflow_test_password1",
             )
         )
 
@@ -298,7 +333,8 @@ def test_interrupt(self):
         self.assertEqual(results["state"], EXEC_STATE_STOPPED)
 
     def test_status_solving(self):
-        execution = self.create_timer_instance_and_execution(5)
+        execution = self.create_timer_instance_and_execution(10)
+        time.sleep(5)
         status = self.client.get_status(execution["id"])
         self.assertEqual(status["state"], EXEC_STATE_RUNNING)
 
@@ -352,7 +388,10 @@ def test_manual_execution2(self):
     def test_edit_one_execution(self):
         one_instance = self.create_new_instance("./cornflow/tests/data/test_mps.mps")
         payload = dict(
-            name="bla", config=dict(solver="CBC"), instance_id=one_instance["id"]
+            name="bla",
+            config=dict(solver="CBC"),
+            instance_id=one_instance["id"],
+            schema="solve_model_dag",
         )
         execution = self.client.create_api("execution/?run=0", json=payload)
         payload = dict(log_text="")
@@ -363,10 +402,12 @@ def test_edit_one_execution(self):
 
 
 class PuLPLogSchema(unittest.TestCase):
-    def solve_model(self, input_data, config):
+    @staticmethod
+    def solve_model(input_data, config):
         return solve_model(input_data, config)
 
-    def dump_progress(self, log_dict):
+    @staticmethod
+    def dump_progress(log_dict):
         LS = LogSchema()
         return LS.load(log_dict)
 
diff --git a/cornflow/tests/ldap/test_ldap_authentication.py b/cornflow/tests/ldap/test_ldap_authentication.py
index 4addbc53..45e00b71 100644
--- a/cornflow/tests/ldap/test_ldap_authentication.py
+++ b/cornflow/tests/ldap/test_ldap_authentication.py
@@ -7,7 +7,7 @@
 import json
 
 # Import from internal modules
-from cornflow.commands import RegisterRoles, AccessInitialization
+from cornflow.app import create_app, register_roles, access_init
 from cornflow.models import UserModel, UserRoleModel
 from cornflow.shared.const import PLANNER_ROLE
 from cornflow.tests.const import SIGNUP_URL, USER_ROLE_URL, USER_URL
@@ -15,8 +15,14 @@
 
 
 class TestLogIn(LoginTestCases.LoginEndpoint):
+    def create_app(self):
+        app = create_app("testing")
+        return app
+
     def setUp(self):
         super().setUp()
+        self.runner = create_app().test_cli_runner()
+        self.runner.invoke(register_roles, ["-v", 1])
         self.AUTH_TYPE = current_app.config["AUTH_TYPE"]
         self.data = {"username": "planner", "password": "planner1234"}
 
@@ -59,7 +65,6 @@ def test_user_table_registration(self):
         self.assertNotEqual(len(user_table_before), len(user_table_after))
 
     def test_role_registration(self):
-        RegisterRoles().run()
         user_role_before = UserRoleModel.query.all()
 
         super().test_successful_log_in()
@@ -70,7 +75,7 @@ def test_role_registration(self):
         self.assertNotEqual(len(user_role_before), len(user_role_after))
 
     def test_restricted_access(self):
-        AccessInitialization().run()
+        self.runner.invoke(access_init)
         super().test_successful_log_in()
         response = self.client.get(
             USER_URL,
@@ -83,7 +88,7 @@ def test_restricted_access(self):
         self.assertEqual(403, response.status_code)
 
     def test_deactivated_endpoint(self):
-        AccessInitialization().run()
+        self.runner.invoke(access_init)
         self.data = {"username": "administrator", "password": "administrator1234"}
         super().test_successful_log_in()
         payload = {"user_id": self.response.json["id"], "role_id": PLANNER_ROLE}
@@ -99,11 +104,11 @@ def test_deactivated_endpoint(self):
         self.assertEqual(501, response.status_code)
 
     def test_deactivated_sign_up(self):
-        RegisterRoles().run()
+        self.runner.invoke(register_roles)
         payload = {
             "username": "testuser",
             "email": "testemail@example.org",
-            "password": "testpassword",
+            "password": "Testpassword1!",
         }
         response = self.client.post(
             SIGNUP_URL,
diff --git a/cornflow/tests/unit/test_cases.py b/cornflow/tests/unit/test_cases.py
index d6df3507..1abcf2b4 100644
--- a/cornflow/tests/unit/test_cases.py
+++ b/cornflow/tests/unit/test_cases.py
@@ -125,6 +125,7 @@ def setUp(self):
             "description": "test case for unit tests",
             "instance_id": instance_id,
             "execution_id": execution_id,
+            "schema": "solve_model_dag",
         }
         self.user_object = UserModel.get_one_user(self.user)
         self.instance = InstanceModel.get_one_object_from_user(
@@ -391,11 +392,11 @@ def setUp(self):
         self.payloads = [self.load_file(f) for f in CASES_LIST]
         self.items_to_check = ["name", "description", "schema"]
         self.url = CASE_URL
-        self.patch = {
-            "data_patch": jsonpatch.make_patch(
+        self.patch = dict(
+            data_patch=jsonpatch.make_patch(
                 self.payloads[0]["data"], self.payloads[1]["data"]
             ).patch
-        }
+        )
         self.patch_file = self.load_file(JSON_PATCH_GOOD_PATH)
 
     def test_json_patch(self):
@@ -548,6 +549,7 @@ def setUp(self):
         self.cases_id = [
             self.create_new_row(self.url, self.model, p) for p in self.payloads
         ]
+
         self.items_to_check = ["name", "description", "schema"]
 
     def test_get_full_patch(self):
diff --git a/cornflow/tests/unit/test_commands.py b/cornflow/tests/unit/test_commands.py
index 1416c698..9cc0be54 100644
--- a/cornflow/tests/unit/test_commands.py
+++ b/cornflow/tests/unit/test_commands.py
@@ -1,18 +1,26 @@
+from flask import current_app
 from flask_testing import TestCase
 
-from cornflow.app import create_app
-from cornflow.commands import (
-    RegisterBasePermissions,
-    CreateAdminUser,
-    CreateServiceUser,
-    RegisterActions,
-    RegisterRoles,
-    RegisterViews,
+from cornflow.app import (
+    access_init,
+    create_admin_user,
+    create_app,
+    create_base_user,
+    create_service_user,
+    register_actions,
+    register_dag_permissions,
+    register_roles,
+    register_views,
 )
+
+from cornflow.commands.dag import register_deployed_dags_command_test
+
 from cornflow.endpoints import resources
 from cornflow.models import (
     ActionModel,
     ApiViewModel,
+    DeployedDAG,
+    PermissionsDAG,
     PermissionViewRoleModel,
     RoleModel,
     UserModel,
@@ -28,51 +36,101 @@
 class TestCommands(TestCase):
     def create_app(self):
         app = create_app("testing")
+        app.config["OPEN_DEPLOYMENT"] = 1
         return app
 
     def setUp(self):
         db.create_all()
         self.payload = {
             "email": "testemail@test.org",
-            "password": "testpassword",
+            "password": "Testpassword1!",
         }
         self.resources = resources
-        RegisterRoles().run(0)
+        self.runner = create_app().test_cli_runner()
+        self.runner.invoke(register_roles, ["-v", 1])
 
     def tearDown(self):
         db.session.remove()
         db.drop_all()
 
-    def test_service_user_command(self):
-        command = CreateServiceUser()
-
-        command.run(
-            username="cornflow",
-            email=self.payload["email"],
-            password=self.payload["password"],
+    def user_command(self, command, username, email):
+        self.runner.invoke(
+            command,
+            [
+                "-u",
+                username,
+                "-e",
+                email,
+                "-p",
+                self.payload["password"],
+            ],
         )
 
-        user = UserModel.get_one_user_by_email("testemail@test.org")
+        user = UserModel.get_one_user_by_email(email)
 
         self.assertNotEqual(None, user)
-        self.assertEqual(self.payload["email"], user.email)
+        self.assertEqual(email, user.email)
         return user
 
+    def user_missing_arguments(self, command):
+        result = self.runner.invoke(
+            command,
+            [
+                "-e",
+                self.payload["email"],
+                "-p",
+                self.payload["password"],
+            ],
+        )
+        self.assertEqual(2, result.exit_code)
+        self.assertIn("Missing option '-u' / '--username'", result.output)
+
+        result = self.runner.invoke(
+            command,
+            [
+                "-u",
+                "cornflow",
+                "-p",
+                self.payload["password"],
+            ],
+        )
+        self.assertEqual(2, result.exit_code)
+        self.assertIn("Missing option '-e' / '--email'", result.output)
+
+        result = self.runner.invoke(
+            command,
+            [
+                "-u",
+                "cornflow",
+                "-e",
+                self.payload["email"],
+            ],
+        )
+        self.assertEqual(2, result.exit_code)
+        self.assertIn("Missing option '-p' / '--password'", result.output)
+
+    def test_service_user_command(self):
+        return self.user_command(create_service_user, "cornflow", self.payload["email"])
+
     def test_service_user_existing_admin(self):
         self.test_admin_user_command()
-        command = CreateServiceUser()
-
-        command.run(
-            username="administrator",
-            email=self.payload["email"],
-            password=self.payload["password"],
+        self.runner.invoke(
+            create_service_user,
+            [
+                "-u",
+                "cornflow",
+                "-e",
+                self.payload["email"],
+                "-p",
+                self.payload["password"],
+            ],
         )
 
         user = UserModel.get_one_user_by_email("testemail@test.org")
 
         self.assertNotEqual(None, user)
         self.assertEqual(self.payload["email"], user.email)
-        self.assertEqual("administrator", user.username)
+        self.assertEqual("cornflow", user.username)
         # TODO: check the user has both roles
 
     def test_service_user_existing_service(self):
@@ -83,23 +141,13 @@ def test_service_user_existing_service(self):
         # TODO: check the user has the role
 
     def test_admin_user_command(self):
-        command = CreateAdminUser()
-
-        command.run(
-            username="administrator",
-            email=self.payload["email"],
-            password=self.payload["password"],
-        )
-
-        user = UserModel.get_one_user_by_email("testemail@test.org")
+        return self.user_command(create_admin_user, "admin", "admin@test.org")
 
-        self.assertNotEqual(None, user)
-        self.assertEqual(self.payload["email"], user.email)
-        return user
+    def test_base_user_command(self):
+        return self.user_command(create_admin_user, "base", "base@test.org")
 
     def test_register_actions(self):
-        command = RegisterActions()
-        command.run()
+        self.runner.invoke(register_actions)
 
         actions = ActionModel.query.all()
 
@@ -107,8 +155,7 @@ def test_register_actions(self):
             self.assertEqual(ACTIONS_MAP[a.id], a.name)
 
     def test_register_views(self):
-        command = RegisterViews()
-        command.run()
+        self.runner.invoke(register_views)
 
         views = ApiViewModel.query.all()
         views_list = [v.name for v in views]
@@ -119,26 +166,12 @@ def test_register_views(self):
         self.assertCountEqual(views_list, resources_list)
 
     def test_register_roles(self):
-        command = RegisterRoles()
-        command.run()
-
         roles = RoleModel.query.all()
-
         for r in roles:
             self.assertEqual(ROLES_MAP[r.id], r.name)
 
     def test_base_permissions_assignation(self):
-        command = RegisterActions()
-        command.run()
-
-        command = RegisterViews()
-        command.run()
-
-        command = RegisterRoles()
-        command.run()
-
-        command = RegisterBasePermissions()
-        command.run()
+        self.runner.invoke(access_init)
 
         for base in BASE_PERMISSION_ASSIGNATION:
             for view in self.resources:
@@ -152,11 +185,64 @@ def test_base_permissions_assignation(self):
 
                     self.assertEqual(True, permission)
 
+    def test_deployed_dags_test_command(self):
+        register_deployed_dags_command_test()
+        dags = DeployedDAG.get_all_objects()
+        for dag in ["solve_model_dag", "gc", "timer"]:
+            self.assertIn(dag, [d.id for d in dags])
+
+    def test_dag_permissions_command(self):
+        register_deployed_dags_command_test()
+        self.test_service_user_command()
+        self.test_admin_user_command()
+        self.runner.invoke(register_dag_permissions, ["-o", 1])
+
+        service = UserModel.get_one_user_by_email("testemail@test.org")
+        admin = UserModel.get_one_user_by_email("admin@test.org")
+
+        service_permissions = PermissionsDAG.get_user_dag_permissions(service.id)
+        admin_permissions = PermissionsDAG.get_user_dag_permissions(admin.id)
+
+        self.assertEqual(3, len(service_permissions))
+        self.assertEqual(3, len(admin_permissions))
+
+    def test_dag_permissions_command_no_open(self):
+        register_deployed_dags_command_test()
+        self.test_service_user_command()
+        self.test_admin_user_command()
+        self.runner.invoke(register_dag_permissions, ["-o", 0])
+
+        service = UserModel.get_one_user_by_email("testemail@test.org")
+        admin = UserModel.get_one_user_by_email("admin@test.org")
+
+        service_permissions = PermissionsDAG.get_user_dag_permissions(service.id)
+        admin_permissions = PermissionsDAG.get_user_dag_permissions(admin.id)
+
+        self.assertEqual(3, len(service_permissions))
+        self.assertEqual(0, len(admin_permissions))
+
     def test_argument_parsing_correct(self):
-        command = RegisterRoles()
-        command.run(verbose="0")
+        self.test_service_user_command()
+        result = self.runner.invoke(register_dag_permissions, ["-o", "a"])
 
-        roles = RoleModel.query.all()
+        self.assertEqual(2, result.exit_code)
+        self.assertIn("is not a valid integer", result.output)
 
-        for r in roles:
-            self.assertEqual(ROLES_MAP[r.id], r.name)
+        service = UserModel.get_one_user_by_email("testemail@test.org")
+        service_permissions = PermissionsDAG.get_user_dag_permissions(service.id)
+        self.assertEqual(0, len(service_permissions))
+
+    def test_argument_parsing_correct_verbose(self):
+        self.test_service_user_command()
+        result = self.runner.invoke(register_dag_permissions, ["-o", 1, "-v", "a"])
+        self.assertEqual(2, result.exit_code)
+        self.assertIn("is not a valid integer", result.output)
+
+    def test_missing_required_argument_service(self):
+        self.user_missing_arguments(create_service_user)
+
+    def test_missing_required_argument_admin(self):
+        self.user_missing_arguments(create_admin_user)
+
+    def test_missing_required_argument_user(self):
+        self.user_missing_arguments(create_base_user)
diff --git a/cornflow/tests/unit/test_dags.py b/cornflow/tests/unit/test_dags.py
index 5da9e91d..fc9f5cad 100644
--- a/cornflow/tests/unit/test_dags.py
+++ b/cornflow/tests/unit/test_dags.py
@@ -4,14 +4,26 @@
 
 # Import from libraries
 import json
+from flask_testing import TestCase
 
 # Import from internal modules
+from cornflow.app import create_app
+from cornflow.commands.access import access_init_command
+from cornflow.commands.dag import register_deployed_dags_command_test
+from cornflow.commands.permissions import register_dag_permissions_command
+from cornflow.shared.const import ADMIN_ROLE, SERVICE_ROLE
+from cornflow.models import DeployedDAG, PermissionsDAG, UserModel, UserRoleModel
 from cornflow.shared.const import EXEC_STATE_CORRECT, EXEC_STATE_MANUAL
+from cornflow.shared.utils import db
 from cornflow.tests.const import (
+    CASE_PATH,
     DAG_URL,
+    DEPLOYED_DAG_URL,
     EXECUTION_URL_NORUN,
-    CASE_PATH,
     INSTANCE_URL,
+    LOGIN_URL,
+    SIGNUP_URL,
+    USER_URL,
 )
 from cornflow.tests.unit.test_executions import TestExecutionsDetailEndpointMock
 
@@ -108,3 +120,146 @@ def test_get_dag(self):
         self.assertEqual(data["data"], instance_data["data"])
         self.assertEqual(data["config"], self.payload["config"])
         return
+
+
+class TestDeployedDAG(TestCase):
+    def create_app(self):
+        app = create_app("testing")
+        return app
+
+    def setUp(self):
+        db.create_all()
+        access_init_command(0)
+        register_deployed_dags_command_test(verbose=0)
+        self.url = USER_URL
+        self.model = UserModel
+        self.admin = dict(
+            username="anAdminUser", email="admin@admin.com", password="Testpassword1!"
+        )
+
+        response = self.client.post(
+            SIGNUP_URL,
+            data=json.dumps(self.admin),
+            follow_redirects=True,
+            headers={"Content-Type": "application/json"},
+        )
+
+        data = dict(self.admin)
+        data.pop("email")
+        self.admin["id"] = response.json["id"]
+
+        self.token = self.client.post(
+            LOGIN_URL,
+            data=json.dumps(data),
+            follow_redirects=True,
+            headers={"Content-Type": "application/json"},
+        ).json["token"]
+
+        user_role = UserRoleModel({"user_id": self.admin["id"], "role_id": ADMIN_ROLE})
+        user_role.save()
+        db.session.commit()
+
+        user_role = UserRoleModel(
+            {"user_id": self.admin["id"], "role_id": SERVICE_ROLE}
+        )
+        user_role.save()
+        db.session.commit()
+
+        register_dag_permissions_command(verbose=0)
+
+    def tearDown(self):
+        db.session.remove()
+        db.drop_all()
+
+    def test_permission_cascade_deletion(self):
+        before = PermissionsDAG.get_user_dag_permissions(self.admin["id"])
+        self.assertIsNotNone(before)
+        dag = DeployedDAG.query.get("solve_model_dag")
+        dag.delete()
+        after = PermissionsDAG.get_user_dag_permissions(self.admin["id"])
+        self.assertNotEqual(before, after)
+        self.assertGreater(len(before), len(after))
+
+    def test_get_deployed_dags(self):
+        response = self.client.get(
+            DEPLOYED_DAG_URL,
+            follow_redirects=True,
+            headers={
+                "Content-Type": "application/json",
+                "Authorization": "Bearer " + self.token,
+            },
+        )
+
+        self.assertEqual(response.status_code, 200)
+        self.assertEqual(
+            response.json,
+            [
+                {"description": None, "id": "solve_model_dag"},
+                {"description": None, "id": "gc"},
+                {"description": None, "id": "timer"},
+            ],
+        )
+
+    def test_endpoint_permissions(self):
+        user_role = UserRoleModel.query.filter_by(
+            user_id=self.admin["id"], role_id=SERVICE_ROLE
+        )
+        user_role.delete()
+        db.session.commit()
+        data = self.client.get(
+            DEPLOYED_DAG_URL,
+            follow_redirects=True,
+            headers={
+                "Content-Type": "application/json",
+                "Authorization": "Bearer " + self.token,
+            },
+        )
+
+        self.assertEqual(data.status_code, 403)
+        self.assertEqual(
+            data.json, {"error": "You do not have permission to access this endpoint"}
+        )
+
+    def test_post_endpoint(self):
+        payload = {"description": None, "id": "test_dag"}
+        response = self.client.post(
+            DEPLOYED_DAG_URL,
+            data=json.dumps(payload),
+            follow_redirects=True,
+            headers={
+                "Content-Type": "application/json",
+                "Authorization": "Bearer " + self.token,
+            },
+        )
+
+        self.assertEqual(response.status_code, 201)
+        self.assertEqual(response.json, payload)
+
+        response = self.client.get(
+            DEPLOYED_DAG_URL,
+            follow_redirects=True,
+            headers={
+                "Content-Type": "application/json",
+                "Authorization": "Bearer " + self.token,
+            },
+        )
+
+        self.assertEqual(response.status_code, 200)
+        self.assertIn(payload, response.json)
+
+    def test_post_not_valid(self):
+        payload = {"description": "test_description"}
+        response = self.client.post(
+            DEPLOYED_DAG_URL,
+            data=json.dumps(payload),
+            follow_redirects=True,
+            headers={
+                "Content-Type": "application/json",
+                "Authorization": "Bearer " + self.token,
+            },
+        )
+        self.assertEqual(response.status_code, 400)
+        self.assertEqual(
+            response.json,
+            {"error": "{'json': {'id': ['Missing data for required field.']}}"},
+        )
diff --git a/cornflow/tests/unit/test_health.py b/cornflow/tests/unit/test_health.py
index 34d32aa4..d947cabf 100644
--- a/cornflow/tests/unit/test_health.py
+++ b/cornflow/tests/unit/test_health.py
@@ -3,7 +3,7 @@
 from cornflow.app import create_app
 from cornflow.shared.utils import db
 from cornflow.tests.const import HEALTH_URL
-from cornflow.shared.const import STATUS_HEALTHY, STATUS_UNHEALTHY
+from cornflow.shared.const import STATUS_HEALTHY
 
 
 class TestHealth(TestCase):
diff --git a/cornflow/tests/unit/test_log_in.py b/cornflow/tests/unit/test_log_in.py
index c6c82531..81618386 100644
--- a/cornflow/tests/unit/test_log_in.py
+++ b/cornflow/tests/unit/test_log_in.py
@@ -19,15 +19,15 @@ def setUp(self):
         self.data = {
             "username": "testname",
             "email": "test@test.com",
-            "password": "testpassword",
+            "password": "Testpassword1!",
         }
         user = UserModel(data=self.data)
         user.save()
         db.session.commit()
         # we take out the email, we do not need it to log in
         self.data.pop("email")
-        self.id = UserModel.query.filter_by(username="testname").first().id
+        self.idx = UserModel.query.filter_by(username="testname").first().id
 
     def test_successful_log_in(self):
         super().test_successful_log_in()
-        self.assertEqual(self.id, self.response.json["id"])
+        self.assertEqual(self.idx, self.response.json["id"])
diff --git a/cornflow/tests/unit/test_permissions.py b/cornflow/tests/unit/test_permissions.py
index 104ca7f1..f47a5dfa 100644
--- a/cornflow/tests/unit/test_permissions.py
+++ b/cornflow/tests/unit/test_permissions.py
@@ -1,19 +1,25 @@
 """
 Unit test for the permissions table
 """
+
 # Import from libraries
 import json
 
 # Import from internal modules
+from cornflow.app import create_app
 from cornflow.endpoints import (
     PermissionsViewRoleEndpoint,
     PermissionsViewRoleDetailEndpoint,
 )
 from cornflow.models import (
-    PermissionViewRoleModel
+    ActionModel,
+    ApiViewModel,
+    InstanceModel,
+    PermissionViewRoleModel,
+    RoleModel,
 )
-from cornflow.shared.const import ROLES_MAP
-from cornflow.tests.const import PERMISSION_URL
+from cornflow.shared.const import GET_ACTION, ROLES_MAP, VIEWER_ROLE
+from cornflow.tests.const import INSTANCE_PATH, INSTANCE_URL, PERMISSION_URL, ROLES_URL
 from cornflow.tests.custom_test_case import CustomTestCase
 
 
@@ -183,3 +189,55 @@ def test_delete_permission_not_authorized(self):
                     },
                 )
                 self.assertEqual(403, response.status_code)
+
+
+class TestPermissionsDagNoOpen(CustomTestCase):
+    def create_app(self):
+        app = create_app("testing")
+        app.config["OPEN_DEPLOYMENT"] = 0
+        return app
+
+    def setUp(self):
+        super().setUp()
+        self.url = INSTANCE_URL
+        self.model = InstanceModel
+
+        def load_file(_file):
+            with open(_file) as f:
+                temp = json.load(f)
+            return temp
+
+        self.payload = load_file(INSTANCE_PATH)
+
+    def test_create_instance_no_permissions(self):
+        self.create_new_row(self.url, self.model, self.payload, 403, False)
+
+    def test_missing_schema(self):
+        self.payload.pop("schema")
+        self.create_new_row(self.url, self.model, self.payload, 400, False)
+
+
+class TestPermissionsViewModel(CustomTestCase):
+    def setUp(self):
+        super().setUp()
+
+    def test_permission_role_cascade_deletion(self):
+        before_permissions = PermissionViewRoleModel.get_all_objects()
+        role = RoleModel.get_one_object(VIEWER_ROLE)
+        role.delete()
+        after_permissions = PermissionViewRoleModel.get_all_objects()
+        self.assertNotEqual(before_permissions, after_permissions)
+
+    def test_permission_action_cascade_deletion(self):
+        before_permissions = PermissionViewRoleModel.get_all_objects()
+        action = ActionModel.get_one_object(GET_ACTION)
+        action.delete()
+        after_permissions = PermissionViewRoleModel.get_all_objects()
+        self.assertNotEqual(before_permissions, after_permissions)
+
+    def test_permission_api_view_cascade_deletion(self):
+        before_permissions = PermissionViewRoleModel.get_all_objects()
+        api_view = ApiViewModel.get_one_by_name("instance")
+        api_view.delete()
+        after_permissions = PermissionViewRoleModel.get_all_objects()
+        self.assertNotEqual(before_permissions, after_permissions)
diff --git a/cornflow/tests/unit/test_roles.py b/cornflow/tests/unit/test_roles.py
index 6d648dcb..28f85f49 100644
--- a/cornflow/tests/unit/test_roles.py
+++ b/cornflow/tests/unit/test_roles.py
@@ -331,11 +331,11 @@ def test_delete_and_create_user_role_authorized_user(self):
         data = {
             "username": "testuser" + str(role),
             "email": "testemail" + str(role) + "@test.org",
-            "password": "testpassword",
+            "password": "Testpassword1!",
         }
         user_response = self.create_user(data)
 
-        self.create_role(user_response.json["id"], role)
+        self.assign_role(user_response.json["id"], role)
 
         self.client.delete(
             self.url + str(user_response.json["id"]) + "/" + str(PLANNER_ROLE) + "/",
@@ -371,20 +371,20 @@ def test_delete_all_user_roles(self):
         data = {
             "username": "testuser",
             "email": "testemail" + "@test.org",
-            "password": "testpassword",
+            "password": "Testpassword1!",
         }
         user_response = self.create_user(data)
         user_id = user_response.json["id"]
         # give it all roles:
         for role in ROLES_MAP:
-            self.create_role(user_id, role)
+            self.assign_role(user_id, role)
         all_roles = UserModel.get_one_user(user_id).roles
         diff = set(r for r in all_roles).symmetric_difference(ROLES_MAP.keys())
         self.assertEqual(len(all_roles), len(ROLES_MAP))
         self.assertEqual(len(diff), 0)
         UserRoleModel.del_one_user(user_id)
-        all_roles = UserModel.get_one_user(user_id).roles
-        self.assertEqual(all_roles, {})
+        all_roles = UserRoleModel.get_one_user(user_id)
+        self.assertEqual(all_roles, [])
 
 
 class TestRolesModelMethods(CustomTestCase):
diff --git a/cornflow/tests/unit/test_schemas.py b/cornflow/tests/unit/test_schemas.py
index a23ec9ae..85965132 100644
--- a/cornflow/tests/unit/test_schemas.py
+++ b/cornflow/tests/unit/test_schemas.py
@@ -1,12 +1,21 @@
+"""
+
+"""
+# General imports
 import unittest
-import json
 
-from cornflow.tests.custom_test_case import CustomTestCase
-from cornflow.tests.const import SCHEMA_URL
+# Partial imports
+from marshmallow import ValidationError, Schema, fields
 from unittest.mock import patch
-from cornflow_client.schema.dict_functions import gen_schema, ParameterSchema, sort_dict
+
+# Imports from environment
 from cornflow_client import get_pulp_jsonschema, get_empty_schema
-from marshmallow import ValidationError, Schema, fields
+from cornflow_client.schema.dict_functions import gen_schema, ParameterSchema, sort_dict
+
+# Imports from internal modules
+from cornflow.app import create_app
+from cornflow.tests.const import SCHEMA_URL
+from cornflow.tests.custom_test_case import CustomTestCase
 
 
 class SchemaGenerator(unittest.TestCase):
@@ -67,13 +76,6 @@ class CoefficientsSchema(Schema):
         self.assertRaises(ValidationError, func_error2)
 
     def test_two_classes(self):
-        # class CoefficientsSchema(Schema):
-        #     name = fields.Str(required=True)
-        #     value = fields.Float(required=True)
-        #
-        # class ObjectiveSchema(Schema):
-        #     name = fields.Str(required=False, allow_none=True)
-        #     coefficients = fields.Nested(CoefficientsSchema, many=True, required=True)
 
         dict_params = dict(
             CoefficientsSchema=[
@@ -154,7 +156,7 @@ def setUp(self):
         self.schema = get_pulp_jsonschema()
         self.config = get_empty_schema(dict(timeLimit=1, solvers=["cbc"]))
         self.url = SCHEMA_URL
-        self.schema_name = "pulp"
+        self.schema_name = "solve_model_dag"
 
     def patch_af_client(self, Airflow_mock):
         af_client = Airflow_mock.return_value
@@ -187,15 +189,44 @@ def test_get_schema(self, airflow_init):
         af_client.get_dag_info.assert_called_once()
         af_client.get_schemas_for_dag_name.assert_called_once()
 
-    @patch("cornflow.endpoints.schemas.Airflow.from_config")
-    def test_get_all_schemas(self, airflow_init):
-        af_client = self.patch_af_client(airflow_init)
-        schemas = self.get_one_row(
-            self.url, {}, expected_status=200, check_payload=False
+
+class TestNewSchemaEndpointOpen(CustomTestCase):
+    def setUp(self):
+        super().setUp()
+        self.url = SCHEMA_URL
+
+    def test_get_all_schemas(self):
+        schemas = self.get_one_row(self.url, {}, 200, False)
+        dags = [{"name": dag} for dag in ["solve_model_dag", "gc", "timer"]]
+
+        self.assertEqual(dags, schemas)
+
+
+class TestNewSchemaEndpointNotOpen(CustomTestCase):
+    def create_app(self):
+        app = create_app("testing")
+        app.config["OPEN_DEPLOYMENT"] = 0
+        return app
+
+    def setUp(self):
+        super().setUp()
+        self.url = SCHEMA_URL
+        self.schema_name = "solve_model_dag"
+
+    def test_get_all_schemas(self):
+        schemas = self.get_one_row(self.url, {}, 200, False)
+        self.assertEqual([], schemas)
+
+    def test_get_one_schema(self):
+        schema = self.get_one_row(
+            self.url + "{}/".format(self.schema_name),
+            {},
+            expected_status=403,
+            check_payload=False,
+        )
+        self.assertEqual(
+            {"error": "User does not have permission to access this dag"}, schema
         )
-        self.assertEqual(schemas[0]["name"], self.schema_name)
-        af_client.is_alive.assert_called_once()
-        af_client.get_all_schemas.assert_called_once()
 
 
 if __name__ == "__main__":
diff --git a/cornflow/tests/unit/test_sign_up.py b/cornflow/tests/unit/test_sign_up.py
index ff76a5c3..8235dfba 100644
--- a/cornflow/tests/unit/test_sign_up.py
+++ b/cornflow/tests/unit/test_sign_up.py
@@ -7,8 +7,7 @@
 import json
 
 # Import from internal modules
-from cornflow.app import create_app
-from cornflow.commands import AccessInitialization
+from cornflow.app import create_app, access_init
 from cornflow.models import UserModel, UserRoleModel
 from cornflow.shared.const import PLANNER_ROLE
 from cornflow.shared.utils import db
@@ -23,11 +22,12 @@ def create_app(self):
 
     def setUp(self):
         db.create_all()
-        AccessInitialization().run()
+        self.runner = create_app().test_cli_runner()
+        self.runner.invoke(access_init)
         self.data = {
             "username": "testname",
             "email": "test@test.com",
-            "password": "testpassword",
+            "password": "Testpassword1!",
         }
 
     def tearDown(self):
diff --git a/cornflow/tests/unit/test_users.py b/cornflow/tests/unit/test_users.py
index 5290a322..1171ee94 100644
--- a/cornflow/tests/unit/test_users.py
+++ b/cornflow/tests/unit/test_users.py
@@ -2,14 +2,18 @@
 
 from flask_testing import TestCase
 from cornflow.app import create_app
-from cornflow.commands import AccessInitialization
+from cornflow.commands.access import access_init_command
+from cornflow.commands.dag import register_deployed_dags_command_test
+from cornflow.commands.permissions import register_dag_permissions_command
 from cornflow.models import (
     CaseModel,
     ExecutionModel,
     InstanceModel,
+    PermissionsDAG,
     UserModel,
     UserRoleModel,
 )
+
 from cornflow.shared.const import ADMIN_ROLE, PLANNER_ROLE, SERVICE_ROLE, VIEWER_ROLE
 from cornflow.shared.utils import db
 from cornflow.tests.const import (
@@ -32,39 +36,42 @@ def create_app(self):
 
     def setUp(self):
         db.create_all()
-        AccessInitialization().run()
+        access_init_command(0)
+        register_deployed_dags_command_test(verbose=0)
 
         self.url = USER_URL
         self.model = UserModel
 
         self.viewer = dict(
-            username="aViewer", email="viewer@test.com", password="testpassword"
+            username="aViewer", email="viewer@test.com", password="Testpassword1!"
         )
 
         self.planner = dict(
             username="aPlanner",
             email="test@test.com",
-            password="testpassword",
+            password="Testpassword1!",
             first_name="first_planner",
             last_name="last_planner",
         )
 
         self.planner_2 = dict(
-            username="aSecondPlanner", email="test2@test.com", password="testpassword2"
+            username="aSecondPlanner", email="test2@test.com", password="Testpassword2!"
         )
 
         self.admin = dict(
-            username="anAdminUser", email="admin@admin.com", password="testpassword"
+            username="anAdminUser", email="admin@admin.com", password="Testpassword1!"
         )
 
         self.admin_2 = dict(
-            username="aSecondAdmin", email="admin2@admin2.com", password="testpassword2"
+            username="aSecondAdmin",
+            email="admin2@admin2.com",
+            password="Testpassword2!",
         )
 
         self.service_user = dict(
             username="aServiceUser",
             email="service_user@test.com",
-            password="tpass_service_user",
+            password="Tpass_service_user1",
         )
 
         self.login_keys = ["username", "password"]
@@ -121,6 +128,7 @@ def setUp(self):
                 user_role.save()
 
         db.session.commit()
+        register_dag_permissions_command(verbose=0)
 
     def tearDown(self):
         db.session.remove()
@@ -306,7 +314,7 @@ def test_edit_other_user_info(self):
         self.assertEqual(403, response.status_code)
 
     def test_change_password(self):
-        payload = {"password": "newtestpassword"}
+        payload = {"password": "Newtestpassword1!"}
         response = self.modify_info(self.planner, self.planner, payload)
         self.assertEqual(200, response.status_code)
         self.planner["password"] = payload["password"]
@@ -315,12 +323,12 @@ def test_change_password(self):
         self.assertIsNotNone(response.json["token"])
 
     def test_change_other_user_password(self):
-        payload = {"password": "newtestpassword_2"}
+        payload = {"password": "Newtestpassword_2"}
         response = self.modify_info(self.planner_2, self.planner, payload)
         self.assertEqual(403, response.status_code)
 
     def test_admin_change_password(self):
-        payload = {"password": "newtestpassword_3"}
+        payload = {"password": "Newtestpassword_3"}
         response = self.modify_info(self.admin, self.planner, payload)
         self.assertEqual(200, response.status_code)
         self.planner["password"] = payload["password"]
@@ -329,12 +337,12 @@ def test_admin_change_password(self):
         self.assertIsNotNone(response.json["token"])
 
     def test_service_user_change_password(self):
-        payload = {"password": "newtestpassword_4"}
+        payload = {"password": "Newtestpassword_4"}
         response = self.modify_info(self.service_user, self.planner, payload)
         self.assertEqual(403, response.status_code)
 
     def test_viewer_user_change_password(self):
-        payload = {"password": "newtestpassword_5"}
+        payload = {"password": "Newtestpassword_5"}
         response = self.modify_info(self.viewer, self.viewer, payload)
         self.assertEqual(200, response.status_code)
         self.viewer["password"] = payload["password"]
@@ -350,13 +358,14 @@ def create_app(self):
 
     def setUp(self):
         db.create_all()
-        AccessInitialization().run()
+        access_init_command(0)
+        register_deployed_dags_command_test(verbose=0)
 
         self.url = USER_URL
         self.model = UserModel
 
         self.admin = dict(
-            username="anAdminUser", email="admin@admin.com", password="testpassword"
+            username="anAdminUser", email="admin@admin.com", password="Testpassword1!"
         )
 
         response = self.client.post(
@@ -375,7 +384,7 @@ def setUp(self):
         self.login_keys = ["username", "password"]
 
         self.viewer = dict(
-            username="aViewer", email="viewer@test.com", password="testpassword"
+            username="aViewer", email="viewer@test.com", password="Testpassword1!"
         )
 
         response = self.client.post(
@@ -386,6 +395,7 @@ def setUp(self):
         )
 
         self.viewer["id"] = response.json["id"]
+        register_dag_permissions_command(verbose=0)
 
     def tearDown(self):
         db.session.remove()
@@ -553,3 +563,24 @@ def test_user_no_roles(self):
         role = UserRoleModel.query.filter_by(user_id=self.viewer["id"]).delete()
         user = UserModel.query.get(self.viewer["id"])
         self.assertEqual(user.roles, {})
+
+    def test_permission_dag_cascade(self):
+        response = self.log_in(self.admin)
+        token = response.json["token"]
+        user_id = response.json["id"]
+
+        before = PermissionsDAG.get_user_dag_permissions(user_id)
+        self.assertIsNotNone(before)
+        response = self.client.delete(
+            self.url + str(user_id) + "/",
+            follow_redirects=True,
+            headers={
+                "Content-Type": "application/json",
+                "Authorization": "Bearer " + token,
+            },
+        )
+
+        self.assertEqual(200, response.status_code)
+        after = PermissionsDAG.get_user_dag_permissions(user_id)
+        self.assertEqual([], after)
+        self.assertNotEqual(before, after)