Ejemplo n.º 1
0
 def loop(self):
     conn = Connection()
     logger.debug("Starting Scheduler Loop")
     # Initialize Heartbeat
     if self.heartbeat > 0:
         next_hb = datetime.now() + timedelta(seconds=self.heartbeat)
     while self.running:
         for job_id, job in self.jobs.items():
             # Nextcall overpassed, execute job
             now = datetime.now()
             if now > job["nextcall"]:
                 self.run(job)
                 delta_kwarg = {job["interval_type"]: job["interval"]}
                 # TODO: Reusing 'now' reduces time drifting, but
                 # will make the job fall behind the nextcall if it
                 # takes longer than its delta, and therefore the app
                 # won't run it again until restart.
                 new_nextcall = now + relativedelta(**delta_kwarg)
                 # Update Internal Value
                 self.jobs[job_id]["nextcall"] = new_nextcall
                 # Update Database Entry
                 conn.db["base.cron"].update_one(
                     {"_id": job_id}, {"$set": {
                         "nextcall": new_nextcall
                     }})
         # Handle Heartbeat
         if self.heartbeat > 0:
             if datetime.now() > next_hb:
                 logger.info("Scheduler Heartbeat")
                 next_hb = datetime.now() + timedelta(
                     seconds=self.heartbeat)
         time.sleep(1)
     logger.debug("Scheduler Loop has been terminated")
Ejemplo n.º 2
0
def handle_call(data, uid):
    """
    Take an action according to params values
    TODO: This is a provisory method until
    there's something more sophisticated.
    """

    p = data["params"]
    method = p["method"]
    cls = registry[p["model"]]

    conn = Connection()
    client = conn.cl

    if config.DB_REPLICASET_ENABLE:
        with client.start_session() as session:
            with session.start_transaction():
                env = Environment(uid, session)
                model = cls(env)
                result = call_method(p, model, method)
    else:
        env = Environment(uid)
        model = cls(env)
        result = call_method(p, model, method)
    return result
Ejemplo n.º 3
0
def ensure_root_user():
    """ Create root user if it doesn't exist,
    or ensure its password matches the one
    specified through the environment variables.
    """
    from olaf.tools import config
    from olaf.db import Connection
    from bson import ObjectId
    from werkzeug.security import generate_password_hash

    # Generate hashed password
    passwd = generate_password_hash(config.ROOT_PASSWORD)

    conn = Connection()
    root = conn.db["base.user"].find_one({"_id": root_uid})

    if not root:
        # Create root user
        logger.warning("Root user is not present, creating...")
        conn.db["base.user"].insert_one({
            "_id": root_uid,
            "name": "root",
            "email": "root",
            "password": passwd
        })
    else:
        # Update root user's password
        logger.info("Overwriting root user password")
        conn.db["base.user"].update_one(
            {"_id": root_uid},
            {"$set": {
                "name": "root",
                "email": "root",
                "password": passwd
            }})
Ejemplo n.º 4
0
 def __init__(self, uid, session=None, context=dict()):
     _context = {"uid": uid}
     for key, value in context.items():
         _context[key] = value
     self.context = frozendict(_context)
     self.session = session
     self.registry = registry
     self.conn = Connection()
     self.cache = DocumentCache(session)
Ejemplo n.º 5
0
def start_session():
    conn = Connection()
    client = conn.cl
    with client.start_session() as session:
        with session.start_transaction():
            uid = ObjectId("000000000000000000000000")
            env = Environment(uid, session)
            baseUser = registry["base.user"]
            rootUser = baseUser(env)
            yield rootUser
            session.abort_transaction()
Ejemplo n.º 6
0
    def run(self, job):
        logger.info("Running job {} ({})...".format(job["_id"], job["name"]))

        # Generate context variables
        conn = Connection()
        client = conn.cl

        with client.start_session() as session:
            with session.start_transaction():
                env = Environment(job["_id"], session)
                _locals = {**LOCALS, "env": env}
                safe_eval(job["code"], _locals)
                return
Ejemplo n.º 7
0
def check_access(model_name, operation, uid):
    """ 
    Check if a given user can perform 
    a given operation on a given model
    """

    # Root user bypasses all security checks
    if uid == ObjectId("000000000000000000000000"):
        return

    conn = Connection()
    user = conn.db["base.user"].find_one({"_id": uid})
    if not user:
        raise AccessError("User not found")

    # Search in user/group many2many intermediate collection
    # for groups this is user is related to.
    user_group_rels = conn.db["base.user.group.rel"].find({"user_oid": uid})

    # Create a list of groups this user belongs to
    groups = [rel["group_oid"] for rel in user_group_rels]

    # Abort right here if user doesn't belong to any groups
    if not groups:
        raise AccessError("Access Denied -- Model: '{}' "
                          "Operation: '{}' - User: '******'".format(
                              model_name, operation, user["_id"]))

    # Search for all ACLs associated to all this groups
    acls = conn.db["base.model.access"].find({
        "group_id": {
            "$in": groups
        },
        "model": model_name
    })

    # Compute access
    allow_list = [acl[operation_field_map[operation]] for acl in acls]
    allow = reduce(lambda x, y: x | y, allow_list)

    if not allow:
        # Deny access
        raise AccessError("Access Denied -- Model: '{}' "
                          "Operation: '{}' - User: '******'".format(
                              model_name, operation, user))

    return
Ejemplo n.º 8
0
def token(request):
    """ Handles POST requests on the /api/token endpoint.
    If a valid email and password are provided within the
    JSON body, it responds with an access token.
    """

    try:
        data = request.get_json()
    except BadRequest:
        return JsonResponse({"msg": "Invalid JSON"}, status=400)

    # Fail if data is None
    if data is None:
        return JsonResponse({"msg": "Invalid JSON"}, status=400)

    # Make sure request body is valid
    if not "email" in data or not "password" in data:
        return JsonResponse({"msg": "Malformed Request"}, status=400)

    conn = Connection()
    user = conn.db["base.user"].find_one({"email": data["email"]})

    # Check user exists
    if not user:
        return JsonResponse({"msg": "Bad Username or Password"}, status=401)

    # Check password is valid
    if not check_password_hash(user["password"], data["password"]):
        return JsonResponse({"msg": "Bad Username or Password"}, status=401)

    # Calculate token expiration time
    token_expiration = datetime.datetime.now() + datetime.timedelta(
        seconds=config.JWT_EXPIRATION_TIME)

    # The token payload is composed with the email and the expiration time.
    # There's no need to store it in database.
    payload = {
        "uid": str(user["_id"]),
        "expires": token_expiration.isoformat()
    }

    return JsonResponse({
        "access_token":
        jwt.encode(payload, key=config.SECRET_KEY).decode('utf-8')
    })
Ejemplo n.º 9
0
def check_access(docset, operation, skip_DLS=False):
    """ 
    Check if a given user can perform 
    a given operation on a given model
    """
    # Read docset attributes
    model_name = docset._name
    uid = docset.env.context["uid"]

    # Get session from docset
    session = docset.env.session

    # Root user bypasses all security checks
    if uid == ObjectId("000000000000000000000000"):
        return

    conn = Connection()
    user = conn.db["base.user"].find_one({"_id": uid}, session=session)
    if not user:
        raise AccessError("User not found")

    # Search in user/group many2many intermediate collection
    # for groups this is user is related to.
    user_group_rels = conn.db["base.user.group.rel"].find({"user_oid": uid},
                                                          session=session)

    # Create a list of groups this user belongs to
    groups = [rel["group_oid"] for rel in user_group_rels]

    # Abort right here if user doesn't belong to any groups
    if not groups:
        raise AccessError("Access Denied -- Model: '{}' "
                          "Operation: '{}' - User: '******'".format(
                              model_name, operation, user["_id"]))

    # The following function calls will raise an AccessError
    # if user doesn't have the required privileges to operate
    # the requested model.
    check_ACL(model_name, operation, groups, user, session)
    if not skip_DLS:
        check_DLS(model_name, operation, groups, user, docset, session)

    return
Ejemplo n.º 10
0
    def start(self):
        # Prevent reinitializing scheduler with this method
        if self.jobs is not None:
            return

        # Prevent from starting if explicitly disabled in config
        if config.SCHEDULER_DISABLE:
            logger.warning("Unable to start scheduler (disabled per config)")
            return

        # Prevent from starting while in shell context
        ctx = AppContext()
        if ctx.read("shell"):
            logger.warning("Scheduler is disabled in shell context")
            return

        conn = Connection()

        # Get all active jobs
        jobs = conn.db["base.cron"].find({"active": True})
        # Update all nextcalls older than current time
        for job in jobs:
            now = datetime.now()
            if now > job["nextcall"]:
                delta_kwarg = {job["interval_type"]: job["interval"]}
                new_nextcall = now + relativedelta(**delta_kwarg)
                conn.db["base.cron"].update_one(
                    {"_id": job["_id"]}, {"$set": {
                        "nextcall": new_nextcall
                    }})

        # Iterate again to get updated values
        # and load them into memory
        jobs.rewind()
        self.jobs = dict()
        for job in jobs:
            self.jobs[job["_id"]] = job

        self.running = True
        self.process = threading.Thread(name="SchedulerLoop", target=self.loop)
        self.process.start()
Ejemplo n.º 11
0
def check_ACL(model_name, operation, groups, user, session):
    """
    Computes the ACL (Access Control List)
    Raises AccessError if the given user cannot perform
    the requested operation on the requested model.
    """

    # Search for ACL Rules associated to all of these groups
    # and also for ACL Ruless not associated to any group (Globals)
    conn = Connection()
    acl_rules = conn.db["base.acl"].find(
        {
            "active": True,
            "model": model_name,
            "$or": [{
                "group_id": {
                    "$in": groups
                }
            }, {
                "group_id": None
            }]
        },
        session=session)

    # Compute access
    allow_list = [
        rule[acl_operation_field_map[operation]] for rule in acl_rules
    ]
    if allow_list:
        allow = reduce(lambda x, y: x | y, allow_list)
    else:
        allow = []

    if not allow:
        # Deny access
        raise AccessError("Access Denied -- Model: '{}' "
                          "Operation: '{}' - User: '******'".format(
                              model_name, operation, user["_id"]))

    return
Ejemplo n.º 12
0
    def function_wrapper(*args, **kwargs):
        request = args[0]
        access_token = request.headers.get("Authorization", None)

        # Make sure header is present and it's valid
        if not access_token or not access_token.startswith("Bearer "):
            return JsonResponse(
                {"msg": "Missing or Invalid Authorization Header"}, status=401)

        # Attempt to decode
        try:
            payload = jwt.decode(access_token[7:], key=config.SECRET_KEY)
            assert ({"expires", "uid"} <= set(payload.keys()))
        except Exception:
            return JsonResponse({"msg": "Invalid Token"}, status=401)

        # Check if token is expired
        fmt_str = r"%Y-%m-%dT%H:%M:%S.%f"
        if datetime.datetime.now() > datetime.datetime.strptime(
                payload["expires"], fmt_str):
            return JsonResponse({"msg": "Access Token Has Expired"},
                                status=401)

        # Try to create ObjectID out of str
        try:
            oid = ObjectId(payload["uid"])
        except TypeError:
            return JsonResponse({"msg": "Invalid Token"}, status=401)

        # Verify if user exists in database
        conn = Connection()
        user = conn.db["base.user"].find_one({"_id": oid})

        if not user:
            # Either user was deleted or token was tampered with
            return JsonResponse({"msg": "Invalid Token"}, status=401)

        return func(oid, *args, **kwargs)
Ejemplo n.º 13
0
def build_DLS_query(model_name, operation, groups, user, session):
    """
    Builds the DLS (Document Level Security) query.
    
    This mongo query is the combination of all individual 
    mongo queries that affect the current user for a 
    given operation on a given model.

    DLS rules not associated to any group are considered
    as "global" and they affect everyone.

    The resulting query will have the following format:
    {
        "$and": [
            {"whatever_the_user_requested": "..."},
            {"global_rule_1": "..."},
            {"global_rule_2": "..."},
            {
                "$or": [
                    {"group_rule_1": "..."},
                    {"group_rule_2": "..."}
                ]
            }
        ]
    }

    From the previous example, we can see global rules
    constraint the domain of documents; and so do the
    combination of all user specific rules. However,
    one group specific rule may relax other group
    specific rules previously found (but not a global one).
    """

    # Root user bypasses all security checks
    if user == ObjectId("000000000000000000000000"):
        return False

    conn = Connection()

    # Search for DLS Rules associated to all of these groups
    group_rules = conn.db["base.dls"].find(
        {
            "active": True,
            dls_operation_field_map[operation]: True,
            "model": model_name,
            "group_id": {
                "$in": groups
            }
        },
        session=session)

    # ...and also for DLS Rules not associated to any group (Globals)
    global_rules = conn.db["base.dls"].find(
        {
            "active": True,
            dls_operation_field_map[operation]: True,
            "model": model_name,
            "group_id": None
        },
        session=session)

    # Initialize queries list
    group_queries = []
    global_queries = []

    # Evaluate global expressions and add them to their list
    for rule in global_rules:
        query = safe_eval(rule["query"], {"user": user})
        if not isinstance(query, dict):
            raise ValueError(
                "Invalid query in DLS rule '{}' ({}): '{}'".format(
                    rule.name, rule._id, rule["query"]))
        global_queries.append(query)

    # Evaluate user expressions and add them to their list
    for rule in group_rules:
        query = safe_eval(rule["query"], {"user": user})
        if not isinstance(query, dict):
            raise ValueError(
                "Invalid query in DLS rule '{}' ({}): '{}'".format(
                    rule.name, rule._id, rule["query"]))
        group_queries.append(query)

    # Build query structure
    if len(group_queries) > 0:
        global_queries.append({"$or": group_queries})

    if len(global_queries) > 0:
        q = {"$and": global_queries}
    else:
        return False

    return q
Ejemplo n.º 14
0
import logging
from olaf.fields import BaseField, Identifier, Boolean, NoPersist, RelationalField, One2many, Many2one, Many2many
from olaf.db import Connection
from bson import ObjectId
from olaf import registry
from olaf.security import check_access

logger = logging.getLogger(__name__)
conn = Connection()


class DeletionConstraintError(BaseException):
    pass


class ModelMeta(type):
    """ This class defines the behavior of
    all model classes.

    An instance of a model is always a set
    of documents. Documents should have no
    representation. If we'd like to work
    with a single object, we have to do so
    through a set of a single document.
    """
    def __new__(mcs, cls, bases, dct):
        for k, v in dct.items():
            if isinstance(v, BaseField):
                dct[k].attr = k
        return super().__new__(mcs, cls, bases, dct)
Ejemplo n.º 15
0
    def load_module_data(module_name, module_data):
        """
        Routine for loading data from a given module into database
        """
        def load_data(env, fname, security=False):
            """
            Imports data from a file.
            - If file is in CSV format, then guess the model from the filename.
            - If file is in YAML format, then obtain model names from file contents.
            First row represents columns, remaining rows represent the data matrix.
            """
            # Get filename from abs path
            base = os.path.basename(fname)
            # Split basename in (name, extension)
            split = os.path.splitext(base)

            fields = list()
            data = list()

            if split[1] == ".csv":
                model = split[0] if not security else "base.acl"
                with open(fname) as csv_file:
                    csv_reader = csv.reader(csv_file, delimiter=",")
                    first_line = True
                    for row in csv_reader:
                        if first_line:
                            first_line = False
                            fields = [*row]
                        else:
                            data.append([*row])
                    env[model].load(fields, data)
            elif split[1] in [".yaml", ".yml"]:
                # Parse YAML file
                with open(fname) as yaml_file:
                    parsed_yaml = yaml.safe_load(yaml_file)

                # Iterate over model names
                for model_name in parsed_yaml.keys():
                    model = env[model_name]
                    # Iterate over items (document data)
                    for item in parsed_yaml[model_name]:
                        dict_data = dict()
                        # Iterate over field:value's
                        for field_name, value in item.items():
                            # Skip 'id' field
                            if field_name == "id":
                                dict_data[field_name] = value
                                continue
                            field = model._fields[field_name]
                            if isinstance(field, Many2one):
                                oid = env[field._comodel_name].get(value)
                                if not oid:
                                    raise ValueError(
                                        "Reference {} not found in database".
                                        format(value))
                                dict_data[field_name] = value
                            elif isinstance(field, One2many) or isinstance(
                                    field, Many2many):
                                """
                                YAML Syntax Reference
                                model.name:
                                    -
                                        id: some_ext_id
                                        some_fld: some_val
                                        something_ids:
                                            - create:                                                
                                                fld1: val1
                                                fld2: val2
                                                fld3: val3
                                            - add: ref1
                                            - add: ref2
                                            - replace:
                                                - ref1
                                                - ref2
                                """
                                tuples = list()
                                # TODO: Review and retest create, add and replace
                                # using YAML files. Create unit tests if possible.
                                for dd in value:
                                    # Every item is a dictionary of one element
                                    # {'operation_name': value}
                                    op = next(iter(dd))
                                    if op == "create":
                                        value = dd[op]
                                    elif op == "add":
                                        value = env[field._comodel_name].get(
                                            dd[op])
                                        if not value:
                                            raise ValueError(
                                                "Reference {} not found in database"
                                                .format(dd[op]))
                                    elif op == "replace":
                                        value = list()
                                        for i in dd[op]:
                                            oid = env[field._comodel_name].get(
                                                i)
                                            if not oid:
                                                raise ValueError(
                                                    "Reference {} not found in database"
                                                    .format(i))
                                            value.append(oid)
                                    else:
                                        raise ValueError(
                                            "Operation {} not allowed during YAML data load"
                                            .format(op))
                                    tuples.append((op, value))
                                dict_data[field_name] = tuples
                            else:
                                dict_data[field_name] = value

                        if "id" in dict_data:
                            ref = env[model_name].get(dict_data["id"])
                            if not ref:
                                # Create base.model.data entry
                                try:
                                    dict_data["_id"] = bson.ObjectId()
                                    env["base.model.data"].create({
                                        "name":
                                        dict_data["id"],
                                        "model":
                                        model_name,
                                        "res_id":
                                        dict_data["_id"]
                                    })
                                    del dict_data["id"]
                                    env[model_name].create(dict_data)
                                except Exception:
                                    raise
                            else:
                                # Update found reference
                                del dict_data["id"]
                                ref.write(dict_data)
                        else:
                            # Create record with generic __import__ prefix
                            try:
                                dict_data["_id"] = bson.ObjectId()
                                env["base.model.data"].create({
                                    "name":
                                    "__import__.{}".format(dict_data["_id"]),
                                    "model":
                                    model_name,
                                    "res_id":
                                    dict_data["_id"]
                                })
                                env[model_name].create(dict_data)
                            except Exception:
                                raise

        def load_file_data(env, module_name, module_data):
            """
            Parses module data, gets filenames and calls loader function
            """
            # Get module status
            module = env.conn.db["base.module"].find_one({"name": module_name})

            if not module:
                result = env.conn.db["base.module"].insert_one({
                    "name":
                    module_name,
                    "status":
                    "pending"
                })
                module = env.conn.db["base.module"].find_one(
                    {"_id": result.inserted_id})

            if module["status"] == "pending":
                if "data" in module_data["manifest"]:
                    for _file in module_data["manifest"]["data"]:
                        fname = os.path.join(module_data["path"], module_name,
                                             _file)
                        logger.debug(
                            "Loading data file '{}' for module '{}'".format(
                                _file, module_name))
                        load_data(env, fname)
                if "security" in module_data["manifest"]:
                    for _file in module_data["manifest"]["security"]:
                        fname = os.path.join(module_data["path"], module_name,
                                             _file)
                        logger.debug(
                            "Loading security file '{}' for module '{}'".
                            format(_file, module_name))
                        load_data(env, fname, True)

                # Flag module as installed
                env.conn.db["base.module"].update_one(
                    {"name": module_name}, {"$set": {
                        "status": "installed"
                    }})

        conn = Connection()
        client = conn.cl

        with client.start_session() as session:
            with session.start_transaction():
                # Create environment with session
                env = Environment(root_uid, session)
                load_file_data(env, module_name, module_data)