Esempio n. 1
0
    def __init__(self, tenant, mail, app):
        """Constructor

        :param str tenant: Tenant ID
        :param flask_mail.Mail mail: Application mailer
        :param App app: Flask application
        """
        self.tenant = tenant
        self.mail = mail
        self.app = app
        self.logger = app.logger

        config_handler = RuntimeConfig("dbAuth", self.logger)
        config = config_handler.tenant_config(tenant)

        db_url = config.get('db_url')

        # get password constraints from config
        self.password_constraints = {
            'min_length': config.get('password_min_length', 8),
            'max_length': config.get('password_max_length', -1),
            'constraints': config.get('password_constraints', []),
            'min_constraints': config.get('password_min_constraints', 0),
            'constraints_message': config.get(
                'password_constraints_message',
                "Password does not match constraints"
            )
        }

        db_engine = DatabaseEngine()
        self.config_models = ConfigModels(db_engine, db_url)
        self.User = self.config_models.model('users')
Esempio n. 2
0
    def is_admin(self, identity):
        db_engine = self.handler().db_engine()
        self.config_models = ConfigModels(db_engine, self.handler().conn_str())

        # Extract user infos from identity
        if isinstance(identity, dict):
            username = identity.get('username')
            group = identity.get('group')
        else:
            username = identity
            group = None
        session = self.config_models.session()
        admin_role = self.admin_role_query(username, group, session)
        session.close()

        return admin_role
Esempio n. 3
0
    def setup_models(self):
        config_handler = self.handler()
        self.config = config_handler.config()

        db_engine = config_handler.db_engine()
        self.config_models = ConfigModels(db_engine, config_handler.conn_str())

        self.Group = self.config_models.model('groups')
        self.Permission = self.config_models.model('permissions')
        self.RegistrableGroup = self.config_models.model('registrable_groups')
        self.RegistrationRequest = self.config_models.model(
            'registration_requests')
        self.Resource = self.config_models.model('resources')
        self.ResourceType = self.config_models.model('resource_types')
        self.Role = self.config_models.model('roles')
        self.User = self.config_models.model('users')
        self.UserInfo = self.config_models.model('user_infos')
Esempio n. 4
0
    def __init__(self, tenant, mail, app):
        """Constructor

        :param str tenant: Tenant ID
        :param flask_mail.Mail mail: Application mailer
        :param App app: Flask application
        """
        self.tenant = tenant
        self.mail = mail
        self.app = app
        self.logger = app.logger

        config_handler = RuntimeConfig("dbAuth", self.logger)
        config = config_handler.tenant_config(tenant)

        db_url = config.get('db_url')

        db_engine = DatabaseEngine()
        self.config_models = ConfigModels(db_engine, db_url)
        self.User = self.config_models.model('users')
    def __init__(self, app, handler):
        """Constructor

        :param Flask app: Flask application
        """
        app.add_url_rule(
            "/alkis", "alkis", self.index, methods=["GET"]
        )
        app.add_url_rule(
            "/alkis/new", "new_alkis", self.new, methods=["GET", "POST"]
        )
        app.add_url_rule(
            "/alkis/create", "create_alkis", self.create,
            methods=["GET", "POST"]
        )
        app.add_url_rule(
            "/alkis/edit/<int:index>", "edit_alkis", self.edit,
            methods=["GET", "POST"]
        )
        app.add_url_rule(
            "/alkis/update", "update_alkis", self.update,
            methods=["GET", "POST"]
        )
        app.add_url_rule(
            "/alkis/update/<int:index>", "update_alkis", self.update,
            methods=["GET", "POST"]
        )
        app.add_url_rule(
            "/alkis/delete/<int:index>", "delete_alkis", self.delete,
            methods=["GET", "POST"]
        )
        self.app = app
        self.handler = handler
        config_handler = handler()
        db_engine = config_handler.db_engine()
        self.config_models = ConfigModels(db_engine, config_handler.conn_str(), ["alkis"])
        self.resources = self.config_models.model('resources')
        self.alkis = self.config_models.model('alkis')

        self.plugindir = "plugins/alkis/templates"
class ALKISController():
    """Controller for theme model"""

    def __init__(self, app, handler):
        """Constructor

        :param Flask app: Flask application
        """
        app.add_url_rule(
            "/alkis", "alkis", self.index, methods=["GET"]
        )
        app.add_url_rule(
            "/alkis/new", "new_alkis", self.new, methods=["GET", "POST"]
        )
        app.add_url_rule(
            "/alkis/create", "create_alkis", self.create,
            methods=["GET", "POST"]
        )
        app.add_url_rule(
            "/alkis/edit/<int:index>", "edit_alkis", self.edit,
            methods=["GET", "POST"]
        )
        app.add_url_rule(
            "/alkis/update", "update_alkis", self.update,
            methods=["GET", "POST"]
        )
        app.add_url_rule(
            "/alkis/update/<int:index>", "update_alkis", self.update,
            methods=["GET", "POST"]
        )
        app.add_url_rule(
            "/alkis/delete/<int:index>", "delete_alkis", self.delete,
            methods=["GET", "POST"]
        )
        self.app = app
        self.handler = handler
        config_handler = handler()
        db_engine = config_handler.db_engine()
        self.config_models = ConfigModels(db_engine, config_handler.conn_str(), ["alkis"])
        self.resources = self.config_models.model('resources')
        self.alkis = self.config_models.model('alkis')

        self.plugindir = "plugins/alkis/templates"

    def index(self):
        """Show alkis configs."""
        session = self.config_models.session()
        resources = session.query(self.alkis).order_by(self.alkis.name)
        session.close()
        return render_template(
            "%s/index.html" % self.plugindir, resources=resources, title="ALKIS"
        )

    def new(self):
        """Show empty alkis form."""
        form = ALKISForm()
        form.pgservice.choices = self.get_pgservices()
        form.header_template.choices = self.get_templates()
        return render_template(
            "%s/form.html" % self.plugindir, title="neue ALKIS Konfiguration",
            action=url_for("create_alkis"), form=form
        )

    def create(self):
        """Create alkis config."""
        form = ALKISForm()
        form.pgservice.choices = self.get_pgservices()
        form.header_template.choices = self.get_templates()
        session = self.config_models.session()
        if form.validate_on_submit():
            alkis = self.alkis()
            alkis.name = form.name.data
            alkis.pgservice = ','.join(form.pgservice.data)
            alkis.enable_alkis = form.enable_alkis.data
            alkis.enable_owner = form.enable_owner.data
            alkis.header_template = form.header_template.data
            resource = self.resources()
            resource.type = "alkis"
            resource.name = alkis.name
            try:
                session.add(alkis)
                session.add(resource)
                session.commit()
                flash("ALKIS Konfiguration hinzugefügt.")
                return redirect(url_for("alkis"))
            except InternalError as e:
                flash("InternalError: %s" % e.orig, "error")
            except IntegrityError as e:
                flash("Name existiert bereits! Bitte einen anderen Namen \
                      verwenden.", "error")

        else:
            flash("ALKIS Konfuration konnte nicht erzeugt werden.", "error")

        session.close()
        return render_template(
            "%s/form.html" % self.plugindir, title="neue ALKIS Konfiguration",
            action=url_for("create_alkis"), form=form
        )

    def edit(self, index=None):
        """Edit alkis config."""
        session = self.config_models.session()
        alkis = session.query(self.alkis).filter_by(id=index).first()
        form = ALKISForm(
            pgservice=[alkis.pgservice],
            name=alkis.name,
            enable_alkis=alkis.enable_alkis,
            enable_owner=alkis.enable_owner,
            header_template=alkis.header_template
        )
        form.pgservice.choices = self.get_pgservices()
        form.header_template.choices = self.get_templates()
        session.close()
        return render_template(
            "%s/form.html" % self.plugindir, title="ALKIS Konfiguration bearbeiten",
            action=url_for("update_alkis") + "/" + str(index), form=form
        )

    def update(self, index=None):
        """Update alkis config."""
        form = ALKISForm()
        form.pgservice.choices = self.get_pgservices()
        form.header_template.choices = self.get_templates()
        session = self.config_models.session()
        if form.validate_on_submit():
            alkis = session.query(self.alkis).filter_by(id=index).first()
            resource = session.query(self.resources).filter_by(
                type="alkis", name=alkis.name
            ).first()
            alkis.name = form.name.data
            alkis.pgservice = ','.join(form.pgservice.data)
            alkis.enable_alkis = form.enable_alkis.data
            alkis.enable_owner = form.enable_owner.data
            alkis.header_template = form.header_template.data
            resource.type = "alkis"
            resource.name = alkis.name
            try:
                session.commit()
                flash("ALKIS Konfiguration aktualisiert.")
                return redirect(url_for("alkis"))
            except InternalError as e:
                flash('InternalError: %s' % e.orig, 'error')
            except IntegrityError as e:
                flash('IntegrityError: %s' % e.orig, 'error')

        else:
            flash("ALKIS Konfuration konnte nicht erzeugt werden.", "error")

        session.close()
        return render_template(
            "%s/form.html" % self.plugindir, title="ALKIS Konfiguration bearbeiten",
            action=url_for("update_alkis") + "/" + str(index), form=form
        )

    def delete(self, index=None):
        """Delete alkis config."""
        session = self.config_models.session()
        alkis = session.query(self.alkis).filter_by(id=index).first()
        resource = session.query(self.resources).filter_by(
            type="alkis", name=alkis.name
        ).first()
        try:
            # delete and commit
            session.delete(alkis)
            session.delete(resource)
            session.commit()
            flash("ALKIS Konfiguration wurde gelöscht.", "success")
        except InternalError as e:
            flash("InternalError: %s" % e.orig, "error")
        except IntegrityError as e:
            flash("IntegrityError: %s" % e.orig, "error")

        session.close()
        return redirect(url_for("alkis"))

    def get_pgservices(self):
        """ Returns alkis pgservices."""

        candidates = []
        PGSERVICEFILE = os.environ.get('PGSERVICEFILE')
        if PGSERVICEFILE:
            candidates = [PGSERVICEFILE]

        candidates.append(os.path.expanduser("~/.pg_service.conf"))

        PGSYSCONFDIR = os.environ.get('PGSYSCONFDIR')
        if PGSYSCONFDIR:
            candidates.append(os.path.join(PGSYSCONFDIR, 'pg_service.conf'))

        candidates.append(os.path.join("/etc", 'pg_service.conf'))
        candidates.append(os.path.join("/etc/postgresql-common", 'pg_service.conf'))
        candidates.append(os.path.join("/etc/sysconfig/pgsql", 'pg_service.conf'))

        config = configparser.ConfigParser()
        for candidate in candidates:
            if os.path.exists(candidate):
                config.read(candidate)
                break

        services = []
        for section in config.sections():
            if section.startswith("alkis") or section.endswith("alkis"):
                services.append((section, section))
        return services

    def get_templates(self):
        """ Returns header templates. """
        current_handler = self.handler()
        qwc2_path = current_handler.config().get("qwc2_path")
        template_path = qwc2_path + "assets/templates/alkis"
        templates = []
        for tmpl in os.listdir(template_path):
            if tmpl.startswith("header"):
                templates.append((tmpl, tmpl))
        return sorted(templates)
Esempio n. 7
0
class Controller:
    """Controller base class

    Add routes for specific controller and provide generic RESTful actions.
    """

    # available options for number of resources shown per page
    PER_PAGE_OPTIONS = [10, 25, 50, 100]
    # default number of resources shown per page
    DEFAULT_PER_PAGE = 10

    def __init__(self, resource_name, base_route, endpoint_suffix,
                 templates_dir, app, handler):
        """Constructor

        :param str resource_name: Visible name of resource (e.g. 'User')
        :param str base_route: Base route for this controller (e.g. 'users')
        :param str endpoint_suffix: Suffix for route endpoints (e.g. 'user')
        :param str templates_dir: Subdir for resource templates (e.g. 'users')
        :param Flask app: Flask application
        :param handler: Tenant config handler
        """
        self.resource_name = resource_name
        self.base_route = base_route
        self.endpoint_suffix = endpoint_suffix
        self.templates_dir = "templates/%s" % templates_dir
        self.app = app
        self.logger = app.logger
        self.handler = handler

        self.add_routes(app)

    def add_routes(self, app):
        """Add routes for this controller.

        :param Flask app: Flask application
        """
        base_route = self.base_route
        suffix = self.endpoint_suffix

        # index
        app.add_url_rule('/%s' % base_route,
                         base_route,
                         self.index,
                         methods=['GET'])
        # new
        app.add_url_rule('/%s/new' % base_route,
                         'new_%s' % suffix,
                         self.new,
                         methods=['GET'])
        # create
        app.add_url_rule('/%s' % base_route,
                         'create_%s' % suffix,
                         self.create,
                         methods=['POST'])
        # edit
        app.add_url_rule('/%s/<int:id>/edit' % base_route,
                         'edit_%s' % suffix,
                         self.edit,
                         methods=['GET'])
        # update
        app.add_url_rule('/%s/<int:id>' % base_route,
                         'update_%s' % suffix,
                         self.update,
                         methods=['PUT'])
        # delete
        app.add_url_rule('/%s/<int:id>' % base_route,
                         'destroy_%s' % suffix,
                         self.destroy,
                         methods=['DELETE'])
        # update or delete
        app.add_url_rule('/%s/<int:id>' % base_route,
                         'modify_%s' % suffix,
                         self.modify,
                         methods=['POST'])

    def setup_models(self):
        config_handler = self.handler()
        self.config = config_handler.config()

        db_engine = config_handler.db_engine()
        self.config_models = ConfigModels(db_engine, config_handler.conn_str())

        self.Group = self.config_models.model('groups')
        self.Permission = self.config_models.model('permissions')
        self.RegistrableGroup = self.config_models.model('registrable_groups')
        self.RegistrationRequest = self.config_models.model(
            'registration_requests')
        self.Resource = self.config_models.model('resources')
        self.ResourceType = self.config_models.model('resource_types')
        self.Role = self.config_models.model('roles')
        self.User = self.config_models.model('users')
        self.UserInfo = self.config_models.model('user_infos')

    def resource_pkey(self):
        """Return primary key column name for resource table (default: 'id')"""
        return 'id'

    # index

    def resources_for_index_query(self, search_text, session):
        """Return query for resources list.

        Implement in subclass

        :param str search_text: Search string for filtering
        :param Session session: DB session
        """
        raise NotImplementedError

    def order_by_criterion(self, sort, sort_asc):
        """Return order_by criterion for sorted resources list.

        :param str sort: Column name for sorting
        :param bool sort_asc: Set to sort in ascending order
        """
        return None

    def index(self):
        """Show resources list."""
        self.setup_models()

        session = self.session()

        # get resources query
        search_text = self.search_text_arg()
        query = self.resources_for_index_query(search_text, session)

        # order by sort args
        sort, sort_asc = self.sort_args()
        sort_param = None
        if sort is not None:
            order_by = self.order_by_criterion(sort, sort_asc)
            if order_by is not None:
                if type(order_by) is tuple:
                    # order by multiple sort columns
                    query = query.order_by(None).order_by(*order_by)
                else:
                    # order by single sort column
                    query = query.order_by(None).order_by(order_by)

                sort_param = sort
                if not sort_asc:
                    # append sort direction suffix
                    sort_param = "%s-" % sort
        # else use default order from index query

        # paginate
        page, per_page = self.pagination_args()
        num_pages = math.ceil(query.count() / per_page)
        resources = query.limit(per_page).offset((page - 1) * per_page).all()

        pagination = {
            'page': page,
            'num_pages': num_pages,
            'per_page': per_page,
            'per_page_options': self.PER_PAGE_OPTIONS,
            'per_page_default': self.DEFAULT_PER_PAGE,
            'params': {
                'search': search_text,
                'sort': sort_param
            }
        }

        session.close()

        return render_template('%s/index.html' % self.templates_dir,
                               resources=resources,
                               endpoint_suffix=self.endpoint_suffix,
                               pkey=self.resource_pkey(),
                               search_text=search_text,
                               pagination=pagination,
                               sort=sort,
                               sort_asc=sort_asc,
                               base_route=self.base_route)

    # new

    def new(self):
        """Show new resource form."""
        self.setup_models()
        template = '%s/form.html' % self.templates_dir
        form = self.create_form()
        title = "Add %s" % self.resource_name
        action = url_for('create_%s' % self.endpoint_suffix)

        return render_template(template,
                               title=title,
                               form=form,
                               action=action,
                               method='POST')

    # create

    def create(self):
        """Create new resource."""
        self.setup_models()
        form = self.create_form()
        if form.validate_on_submit():
            try:
                # create and commit resource
                session = self.session()
                self.create_or_update_resources(None, form, session)
                session.commit()
                self.update_config_timestamp(session)
                session.close()
                flash('%s has been created.' % self.resource_name, 'success')

                return redirect(url_for(self.base_route))
            except InternalError as e:
                flash('InternalError: %s' % e.orig, 'error')
            except IntegrityError as e:
                flash('IntegrityError: %s' % e.orig, 'error')
            except ValidationError as e:
                flash('Could not create %s.' % self.resource_name, 'warning')
        else:
            flash('Could not create %s.' % self.resource_name, 'warning')

        # show validation errors
        template = '%s/form.html' % self.templates_dir
        title = "Add %s" % self.resource_name
        action = url_for('create_%s' % self.endpoint_suffix)

        return render_template(template,
                               title=title,
                               form=form,
                               action=action,
                               method='POST')

    # edit

    def find_resource(self, id, session):
        """Find resource by ID.

        Implement in subclass

        :param int id: Resource ID
        :param Session session: DB session
        """
        raise NotImplementedError

    def edit(self, id):
        """Show edit resource form.

        :param int id: Resource ID
        """
        self.setup_models()
        # find resource
        session = self.session()
        resource = self.find_resource(id, session)

        if resource is not None:
            template = '%s/form.html' % self.templates_dir
            form = self.create_form(resource, True)
            session.close()
            title = "Edit %s" % self.resource_name
            action = url_for('update_%s' % self.endpoint_suffix, id=id)

            return render_template(template,
                                   title=title,
                                   form=form,
                                   action=action,
                                   id=id,
                                   method='PUT')
        else:
            # resource not found
            session.close()
            abort(404)

    # update

    def update(self, id):
        """Update existing resource.

        :param int id: Resource ID
        """
        self.setup_models()
        # find resource
        session = self.session()
        resource = self.find_resource(id, session)

        if resource is not None:
            form = self.create_form(resource)
            if form.validate_on_submit():
                try:
                    # update and commit resource
                    self.create_or_update_resources(resource, form, session)
                    session.commit()
                    self.update_config_timestamp(session)
                    session.close()
                    flash('%s has been updated.' % self.resource_name,
                          'success')

                    return redirect(url_for(self.base_route))
                except InternalError as e:
                    flash('InternalError: %s' % e.orig, 'error')
                except IntegrityError as e:
                    flash('IntegrityError: %s' % e.orig, 'error')
                except ValidationError as e:
                    flash('Could not update %s.' % self.resource_name,
                          'warning')
            else:
                flash('Could not update %s.' % self.resource_name, 'warning')

            session.close()

            # show validation errors
            template = '%s/form.html' % self.templates_dir
            title = "Edit %s" % self.resource_name
            action = url_for('update_%s' % self.endpoint_suffix, id=id)

            return render_template(template,
                                   title=title,
                                   form=form,
                                   action=action,
                                   method='PUT')
        else:
            # resource not found
            session.close()
            abort(404)

    # destroy

    def destroy_resource(self, resource, session):
        """Delete existing resource in DB.

        :param object resource: Resource object
        :param Session session: DB session
        """
        session.delete(resource)

    def destroy(self, id):
        """Delete existing resource.

        :param int id: Resource ID
        """
        self.setup_models()
        # find resource
        session = self.session()
        resource = self.find_resource(id, session)

        if resource is not None:
            try:
                # update and commit resource
                self.destroy_resource(resource, session)
                session.commit()
                self.update_config_timestamp(session)
                flash('%s has been deleted.' % self.resource_name, 'success')
            except InternalError as e:
                flash('InternalError: %s' % e.orig, 'error')
            except IntegrityError as e:
                flash('IntegrityError: %s' % e.orig, 'error')

            session.close()

            return redirect(url_for(self.base_route))
        else:
            # resource not found
            session.close()
            abort(404)

    def modify(self, id):
        """Workaround for missing PUT and DELETE methods in HTML forms
        using hidden form parameter '_method'.
        """
        method = request.form.get('_method', '').upper()
        if method == 'PUT':
            return self.update(id)
        elif method == 'DELETE':
            return self.destroy(id)
        else:
            abort(405)

    def create_form(self, resource=None, edit_form=False):
        """Return form for resource with fields loaded from DB.

        Implement in subclass

        :param object resource: Optional resource object
        :param bool edit_form: Set if edit form
        """
        raise NotImplementedError

    def create_or_update_resources(self, resource, form, session):
        """Create or update resource in DB.

        Implement in subclass

        :param object resource: Optional resource object (None for create)
        :param FlaskForm form: Form for resource
        :param Session session: DB session
        """
        raise NotImplementedError

    def session(self):
        """Return new session for ConfigDB."""
        return self.config_models.session()

    def raise_validation_error(self, field, msg):
        """Raise ValidationError for a field.

        :param wtforms.fields.Field field: WTForms field
        :param str msg: Validation error message
        """
        error = ValidationError(msg)
        field.errors.append(error)
        raise error

    def update_config_timestamp(self, session):
        """Update timestamp of last config change to current UTC time.

        :param Session session: DB session
        """
        # get first timestamp record
        LastUpdate = self.config_models.model('last_update')
        query = session.query(LastUpdate)
        last_update = query.first()
        if last_update is None:
            # create new timestamp record
            last_update = self.LastUpdate()
            session.add(last_update)

        # update and commit new timestamp
        last_update.updated_at = datetime.utcnow()
        session.commit()

    def update_form_collection(self, resource, edit_form, multi_select,
                               relation_model, collection_attr, id_attr,
                               name_attr, session):
        """Helper to update collection multi-select field for resource.

        :param object resource: Optional resource object for edit (e.g. group)
        :param bool edit_form: Set if edit form
        :param SelectMultipleField multi_select: MultiSelect for relations
                                                 (e.g. form.users)
        :param object relation_model: ConfigModel for relation (e.g. User)
        :param str collection_attr: Collection attribute for resource
                                    (e.g. 'users_collection')
        :param str id_attr: ID attribute of relation model (e.g. 'id')
        :param str name_attr: Name attribute of relation model (e.g. 'name')
        :param Session session: DB session
        """
        if edit_form:
            # add collection items for resource on edit
            items = getattr(resource, collection_attr)
            multi_select.data = [getattr(i, id_attr) for i in items]

        # load related resources from DB
        query = session.query(relation_model). \
            order_by(getattr(relation_model, name_attr))
        items = query.all()

        # set choices for collection select field
        multi_select.choices = [(getattr(i, id_attr), getattr(i, name_attr))
                                for i in items]

    def update_collection(self, collection, multi_select, relation_model,
                          id_attr, session):
        """Helper to add or remove relations from a resource collection.

        :param object collection: Collection of resource relations
                                  (e.g. Group.user_collection)
        :param SelectMultipleField multi_select: MultiSelect for relations
                                                 (e.g. form.users)
        :param object relation_model: ConfigModel for relation (e.g. User)
        :param str id_attr: ID attribute of relation model (e.g. 'id')
        :param Session session: DB session
        """
        # lookup for relation of resource
        resource_relations = {}
        for relation in collection:
            resource_relations[relation.id] = relation

        # update relations
        relation_ids = []
        for relation_id in multi_select.data:
            # get relation from ConfigDB
            filter = {id_attr: relation_id}
            query = session.query(relation_model).filter_by(**filter)
            relation = query.first()

            if relation is not None:
                relation_ids.append(relation_id)
                if relation_id not in resource_relations:
                    # add relation to resource
                    collection.append(relation)

        # remove removed relations
        for relation in resource_relations.values():
            if relation.id not in relation_ids:
                # remove relation from resource
                collection.remove(relation)

    def search_text_arg(self):
        """Return request arg for search string."""
        search_text = request.args.get('search')
        if not search_text:
            search_text = None

        return search_text

    def sort_args(self):
        """Return request arg for sort as (sort, sort_asc)."""
        sort = request.args.get('sort')
        sort_asc = True
        if not sort:
            sort = None
        else:
            # detect any direction suffix '-', e.g. 'name-'
            parts = sort.split('-')
            sort = parts[0]
            if len(parts) > 1:
                # sort in descending order
                sort_asc = False

        return sort, sort_asc

    def pagination_args(self):
        """Return request args for pagination as (page, per_page)."""
        page = self.to_int(request.args.get('page'), 1, 1)
        per_page = self.to_int(request.args.get('per_page'),
                               self.DEFAULT_PER_PAGE, 1)
        return page, per_page

    def to_int(self, value, default, min_value=None):
        """Convert string value to int.

        :param str value: Input string
        :param int default: Default value if blank or not parseable
        :param int min_value: Optional minimum value
        """
        try:
            result = int(value or default)
            if min_value is not None and result < min_value:
                # clamp to min value
                result = min_value
            return result
        except Exception as e:
            return default
Esempio n. 8
0
class DBAuth:
    """DBAuth class

    Provide user login and password reset with local user database.
    """

    # name of default admin user
    DEFAULT_ADMIN_USER = '******'

    # authentication form fields
    USERNAME = '******'
    PASSWORD = '******'

    def __init__(self, tenant, mail, app):
        """Constructor

        :param str tenant: Tenant ID
        :param flask_mail.Mail mail: Application mailer
        :param App app: Flask application
        """
        self.tenant = tenant
        self.mail = mail
        self.app = app
        self.logger = app.logger

        config_handler = RuntimeConfig("dbAuth", self.logger)
        config = config_handler.tenant_config(tenant)

        db_url = config.get('db_url')

        db_engine = DatabaseEngine()
        self.config_models = ConfigModels(db_engine, db_url)
        self.User = self.config_models.model('users')

    def tenant_prefix(self):
        """URL prefix for tentant"""
        if self.tenant == 'default':
            return '/'
        else:
            return '/' + self.tenant
        # TODO: use tenant_prefix() from TenantHandler

    def login(self):
        """Authorize user and sign in."""
        target_url = request.args.get('url', self.tenant_prefix())
        retry_target_url = request.args.get('url', None)
        self.app.config['SESSION_COOKIE_PATH'] = self.tenant_prefix()

        if POST_PARAM_LOGIN:
            # Pass additional parameter specified
            req = request.form
            queryvals = {}
            for key, val in req.items():
                if key not in (self.USERNAME, self.PASSWORD):
                    queryvals[key] = val
            parts = urlparse(target_url)
            target_query = dict(parse_qsl(parts.query))
            target_query.update(queryvals)
            parts = parts._replace(query=urlencode(target_query))
            target_url = urlunparse(parts)

        self.clear_verify_session()

        if current_user.is_authenticated:
            # Note: This might end up in a loop when a user
            # wants to switch to an other login
            return redirect(target_url)

        # create session for ConfigDB
        db_session = self.db_session()

        if POST_PARAM_LOGIN:
            username = req.get(self.USERNAME)
            password = req.get(self.PASSWORD)
            if username:
                user = self.find_user(db_session, name=username)
                if self.__user_is_authorized(user, password, db_session):
                    return self.response(
                        self.__login_response(user, target_url), db_session
                    )
                else:
                    self.logger.info(
                        "POST_PARAM_LOGIN: Invalid username or password")
                    return self.response(
                        redirect(url_for('login', url=retry_target_url)),
                        db_session
                    )

        form = LoginForm()
        if form.validate_on_submit():
            user = self.find_user(db_session, name=form.username.data)

            # force password change on first sign in of default admin user
            # NOTE: user.last_sign_in_at will be set after successful auth
            force_password_change = (
                user and user.name == self.DEFAULT_ADMIN_USER
                and user.last_sign_in_at is None
            )

            if self.__user_is_authorized(user, form.password.data, db_session):
                if not force_password_change:
                    if TOTP_ENABLED:
                        session['login_uid'] = user.id
                        session['target_url'] = target_url
                        if user.totp_secret:
                            # show form for verification token
                            return self.response(
                                 self.__verify(db_session, False), db_session
                            )
                        else:
                            # show form for TOTP setup on first sign in
                            return self.response(
                                self.__setup_totp(db_session, False),
                                db_session
                            )
                    else:
                        # login successful
                        return self.response(
                            self.__login_response(user, target_url), db_session
                        )
                else:
                    return self.response(
                        self.require_password_change(
                            user, target_url, db_session
                        ),
                        db_session
                    )
            else:
                flash('Invalid username or password')
                return self.response(
                    redirect(url_for('login', url=retry_target_url)),
                    db_session
                )

        return self.response(
            render_template('login.html', title='Sign In', form=form),
            db_session
        )

    def verify(self):
        """Handle submit of form for TOTP verification token."""
        # create session for ConfigDB
        db_session = self.db_session()

        return self.response(self.__verify(db_session), db_session)

    def __verify(self, db_session, submit=True):
        """Show form for TOTP verification token.

        :param Session db_session: DB session
        :param bool submit: Whether form was submitted
                            (False if shown after login form)
        """
        if not TOTP_ENABLED or 'login_uid' not in session:
            # TOTP not enabled or not in login process
            return redirect(url_for('login'))

        user = self.find_user(db_session, id=session.get('login_uid', None))
        if user is None:
            # user not found
            return redirect(url_for('login'))

        form = VerifyForm()
        if submit and form.validate_on_submit():
            if self.user_totp_is_valid(user, form.token.data, db_session):
                # TOTP verified
                target_url = session.pop('target_url', '/')
                self.clear_verify_session()
                return self.__login_response(user, target_url)
            else:
                flash('Invalid verification code')
                form.token.errors.append('Invalid verification code')
                form.token.data = None

            if user.failed_sign_in_count >= MAX_LOGIN_ATTEMPTS:
                # redirect to login after too many login attempts
                return redirect(url_for('login'))

        return render_template('verify.html', title='Sign In', form=form)

    def logout(self):
        """Sign out."""
        self.clear_verify_session()
        target_url = request.args.get('url', self.tenant_prefix())
        resp = make_response(redirect(target_url))
        self.app.config['JWT_ACCESS_COOKIE_PATH'] = self.tenant_prefix()
        unset_jwt_cookies(resp)
        self.app.config['SESSION_COOKIE_PATH'] = self.tenant_prefix()
        logout_user()
        return resp

    def setup_totp(self):
        """Handle submit of form with TOTP QR Code and token confirmation."""
        # create session for ConfigDB
        db_session = self.db_session()

        return self.response(self.__setup_totp(db_session), db_session)

    def __setup_totp(self, db_session, submit=True):
        """Show form with TOTP QR Code and token confirmation.

        :param Session db_session: DB session
        :param bool submit: Whether form was submitted
                            (False if shown after login form)
        """
        if not TOTP_ENABLED or 'login_uid' not in session:
            # TOTP not enabled or not in login process
            return redirect(url_for('login'))

        user = self.find_user(db_session, id=session.get('login_uid', None))
        if user is None:
            # user not found
            return redirect(url_for('login'))

        totp_secret = session.get('totp_secret', None)
        if totp_secret is None:
            # generate new secret
            totp_secret = pyotp.random_base32()
            # store temp secret in session
            session['totp_secret'] = totp_secret

        form = VerifyForm()
        if submit and form.validate_on_submit():
            if pyotp.totp.TOTP(totp_secret).verify(
                form.token.data, valid_window=1
            ):
                # TOTP confirmed

                # save TOTP secret
                user.totp_secret = totp_secret
                # update last sign in timestamp and reset failed attempts
                # counter
                user.last_sign_in_at = datetime.utcnow()
                user.failed_sign_in_count = 0
                db_session.commit()

                target_url = session.pop('target_url', '/')
                self.clear_verify_session()
                return self.__login_response(user, target_url)
            else:
                flash('Invalid verification code')
                form.token.errors.append('Invalid verification code')
                form.token.data = None

        # enable one-time loading of QR code image
        session['show_qrcode'] = True

        # show form
        resp = make_response(render_template(
            'qrcode.html', title='Two Factor Authentication Setup', form=form,
            totp_secret=totp_secret
        ))
        # do not cache in browser
        resp.headers.set(
            'Cache-Control', 'no-cache, no-store, must-revalidate'
        )
        resp.headers.set('Pragma', 'no-cache')
        resp.headers.set('Expires', '0')

        return resp

    def qrcode(self):
        """Return TOTP QR code."""
        if not TOTP_ENABLED or 'login_uid' not in session:
            # TOTP not enabled or not in login process
            abort(404)

        # check presence of show_qrcode
        # to allow one-time loading from TOTP setup form
        if 'show_qrcode' not in session:
            # not in TOTP setup form
            abort(404)
        # remove show_qrcode from session
        session.pop('show_qrcode', None)

        totp_secret = session.get('totp_secret', None)
        if totp_secret is None:
            # temp secret not set
            abort(404)

        # create session for ConfigDB
        db_session = self.db_session()
        # find user by ID
        user = self.find_user(db_session, id=session.get('login_uid', None))
        # close session
        db_session.close()

        if user is None:
            # user not found
            abort(404)

        # generate TOTP URI
        email = user.email or user.name
        uri = pyotp.totp.TOTP(totp_secret).provisioning_uri(
            email, issuer_name=TOTP_ISSUER_NAME
        )

        # generate QR code
        img = qrcode.make(uri, box_size=6, border=1)
        stream = BytesIO()
        img.save(stream, 'PNG')

        return Response(
                stream.getvalue(),
                content_type='image/png',
                headers={
                    # do not cache in browser
                    'Cache-Control': 'no-cache, no-store, must-revalidate',
                    'Pragma': 'no-cache',
                    'Expires': '0'
                },
                status=200
            )

    def new_password(self):
        """Show form and send reset password instructions."""
        form = NewPasswordForm()
        if form.validate_on_submit():
            # create session for ConfigDB
            db_session = self.db_session()

            user = self.find_user(db_session, email=form.email.data)
            if user:
                # generate and save reset token
                user.reset_password_token = self.generate_token()
                db_session.commit()

                # send password reset instructions
                try:
                    self.send_reset_passwort_instructions(user)
                except Exception as e:
                    self.logger.error(
                        "Could not send reset password instructions to "
                        "user '%s':\n%s" % (user.email, e)
                    )
                    flash("Failed to send reset password instructions")
                    return self.response(
                        render_template(
                            'new_password.html', title='Forgot your password?',
                            form=form
                        ),
                        db_session
                    )

            # NOTE: show message anyway even if email not found
            flash(
                "You will receive an email with instructions on how to reset "
                "your password in a few minutes."
            )
            return self.response(
                redirect(url_for('login')),
                db_session
            )

        return render_template(
            'new_password.html', title='Forgot your password?', form=form
        )

    def edit_password(self, token):
        """Show form and reset password.

        :param str: Password reset token
        """
        form = EditPasswordForm()
        if form.validate_on_submit():
            # create session for ConfigDB
            db_session = self.db_session()

            user = self.find_user(
                db_session, reset_password_token=form.reset_password_token.data
            )
            if user:
                # save new password
                user.set_password(form.password.data)
                # clear token
                user.reset_password_token = None
                if user.last_sign_in_at is None:
                    # set last sign in timestamp after required password change
                    # to mark as password changed
                    user.last_sign_in_at = datetime.utcnow()
                db_session.commit()

                flash("Your password was changed successfully.")
                return self.response(
                    redirect(url_for('login')), db_session
                )
            else:
                # invalid reset token
                flash("Reset password token is invalid")
                return self.response(
                    render_template(
                        'edit_password.html', title='Change your password',
                        form=form
                    ),
                    db_session
                )

        if token:
            # set hidden field
            form.reset_password_token.data = token

        return render_template(
            'edit_password.html', title='Change your password', form=form
        )

    def require_password_change(self, user, target_url, db_session):
        """Show form for required password change.

        :param User user: User instance
        :param str target_url: URL for redirect
        :param Session db_session: DB session
        """
        # clear last sign in timestamp and generate reset token
        # to mark as requiring password change
        user.last_sign_in_at = None
        user.reset_password_token = self.generate_token()
        db_session.commit()

        # show password reset form
        form = EditPasswordForm()
        # set hidden field
        form.reset_password_token.data = user.reset_password_token

        flash("Please choose a new password")
        return render_template(
            'edit_password.html', title='Change your password', form=form
        )

    def db_session(self):
        """Return new session for ConfigDB."""
        return self.config_models.session()

    def response(self, response, db_session):
        """Helper for closing DB session before returning response.

        :param obj response: Response
        :param Session db_session: DB session
        """
        # close session
        db_session.close()

        return response

    def find_user(self, db_session, **kwargs):
        """Find user by filter.

        :param Session db_session: DB session
        :param **kwargs: keyword arguments for filter (e.g. name=username)
        """
        return db_session.query(self.User).filter_by(**kwargs).first()

    def load_user(self, id):
        """Load user by id.

        :param int id: User ID
        """
        # create session for ConfigDB
        db_session = self.db_session()
        # find user by ID
        user = self.find_user(db_session, id=id)
        # close session
        db_session.close()

        return user

    def token_exists(self, token):
        """Check if password reset token exists.

        :param str: Password reset token
        """
        # create session for ConfigDB
        db_session = self.db_session()
        # find user by password reset token
        user = self.find_user(db_session, reset_password_token=token)
        # close session
        db_session.close()

        return user is not None

    def __user_is_authorized(self, user, password, db_session):
        """Check credentials, update user sign in fields and
        return whether user is authorized.

        :param User user: User instance
        :param str password: Password
        :param Session db_session: DB session
        """
        if user is None or user.password_hash is None:
            # invalid username or no password set
            return False
        elif user.check_password(password):
            # valid credentials
            if user.failed_sign_in_count < MAX_LOGIN_ATTEMPTS:
                if not TOTP_ENABLED:
                    # update last sign in timestamp and reset failed attempts
                    # counter
                    user.last_sign_in_at = datetime.utcnow()
                    user.failed_sign_in_count = 0
                    db_session.commit()

                return True
            else:
                # block sign in due to too many login attempts
                return False
        else:
            # invalid password

            # increase failed login attempts counter
            user.failed_sign_in_count += 1
            db_session.commit()

            return False

    def user_totp_is_valid(self, user, token, db_session):
        """Check TOTP token, update user sign in fields and
        return whether user is authorized.

        :param User user: User instance
        :param str token: TOTP token
        :param Session db_session: DB session
        """
        if user is None or not user.totp_secret:
            # invalid user ID or blank TOTP secret
            return False
        elif pyotp.totp.TOTP(user.totp_secret).verify(token, valid_window=1):
            # valid token
            # update last sign in timestamp and reset failed attempts counter
            user.last_sign_in_at = datetime.utcnow()
            user.failed_sign_in_count = 0
            db_session.commit()

            return True
        else:
            # invalid token

            # increase failed login attempts counter
            user.failed_sign_in_count += 1
            db_session.commit()

            return False

    def clear_verify_session(self):
        """Clear session values for TOTP verification."""
        session.pop('login_uid', None)
        session.pop('target_url', None)
        session.pop('totp_secret', None)
        session.pop('show_qrcode', None)

    def __login_response(self, user, target_url):
        self.logger.info("Logging in as user '%s'" % user.name)
        self.app.config['SESSION_COOKIE_PATH'] = self.tenant_prefix()
        # flask_login stores user in session
        login_user(user)

        # Create the tokens we will be sending back to the user
        access_token = create_access_token(identity=user.name)
        # refresh_token = create_refresh_token(identity=username)

        resp = make_response(redirect(target_url))
        # Set the JWTs and the CSRF double submit protection cookies
        # in this response
        self.app.config['JWT_ACCESS_COOKIE_PATH'] = self.tenant_prefix()
        set_access_cookies(resp, access_token)

        return resp

    def generate_token(self):
        """Generate new token."""
        token = None
        while token is None:
            # generate token
            token = base64.urlsafe_b64encode(os.urandom(15)). \
                rstrip(b'=').decode('ascii')

            # check uniqueness of token
            if self.token_exists(token):
                # token already present
                token = None

        return token

    def send_reset_passwort_instructions(self, user):
        """Send mail with reset password instructions to user.

        :param User user: User instance
        """
        # generate full reset password URL
        reset_url = url_for(
            'edit_password', reset_password_token=user.reset_password_token,
            _external=True
        )

        msg = Message(
            "Reset password instructions",
            recipients=[user.email]
        )
        # set message body from template
        msg.body = render_template(
            'reset_password_instructions.txt', user=user, reset_url=reset_url
        )

        # send message
        self.logger.debug(msg)
        self.mail.send(msg)
Esempio n. 9
0
    def __init__(self, config, logger):
        """Constructor

        :param obj config: ConfigGenerator config
        :param Logger logger: Logger
        """
        self.logger = Logger(logger)

        self.config = config
        generator_config = config.get('config', {})
        self.tenant = generator_config.get('tenant', 'default')
        self.logger.info("Using tenant '%s'" % self.tenant)
        # Set output config path for the generated configuration files.
        # If `config_path` is not set in the configGeneratorConfig.json,
        # then either use the `OUTPUT_CONFIG_PATH` ENV variable (if it is set)
        # or default back to the `/tmp/` directory
        self.config_path = generator_config.get(
            'config_path', os.environ.get('OUTPUT_CONFIG_PATH', '/tmp/'))
        self.tenant_path = os.path.join(self.config_path, self.tenant)

        self.temp_config_path = tempfile.mkdtemp(prefix='qwc_')
        self.temp_tenant_path = os.path.join(self.temp_config_path,
                                             self.tenant)

        try:
            # load ORM models for ConfigDB
            config_db_url = generator_config.get(
                'config_db_url', 'postgresql:///?service=qwc_configdb')
            db_engine = DatabaseEngine()
            self.config_models = ConfigModels(db_engine, config_db_url)
        except Exception as e:
            msg = ("Could not load ConfigModels for ConfigDB at '%s':\n%s" %
                   (config_db_url, e))
            self.logger.error(msg)
            raise Exception(msg)

        # load capabilites for all QWC2 theme items
        self.capabilities_reader = CapabilitiesReader(
            generator_config, config.get("themesConfig"), self.logger)
        self.capabilities_reader.preprocess_qgs_projects(
            generator_config, self.tenant)
        self.capabilities_reader.search_qgs_projects(generator_config)
        self.capabilities_reader.load_all_project_settings()

        # lookup for additional service configs by name
        self.service_configs = {}
        for service_config in self.config.get('services', []):
            self.service_configs[service_config['name']] = service_config

        # create service config handlers
        self.config_handler = {
            # services with resources
            'ogc':
            OGCServiceConfig(generator_config,
                             self.capabilities_reader, self.config_models,
                             self.service_config('ogc'), self.logger),
            'mapViewer':
            MapViewerConfig(self.temp_tenant_path, generator_config,
                            self.capabilities_reader, self.config_models,
                            self.service_config('mapViewer'), self.logger),
            'featureInfo':
            FeatureInfoServiceConfig(generator_config,
                                     self.capabilities_reader,
                                     self.config_models,
                                     self.service_config('featureInfo'),
                                     self.logger),
            'print':
            PrintServiceConfig(self.capabilities_reader,
                               self.service_config('print'), self.logger),
            'search':
            SearchServiceConfig(self.config_models,
                                self.service_config('search'), self.logger),
            'legend':
            LegendServiceConfig(generator_config, self.capabilities_reader,
                                self.config_models,
                                self.service_config('legend'), self.logger),
            'data':
            DataServiceConfig(self.service_config('data'), generator_config,
                              self.config_models, self.logger),
            'ext':
            ExtServiceConfig(self.config_models, self.service_config('ext'),
                             self.logger),

            # config-only services
            'adminGui':
            ServiceConfig(
                'adminGui',
                'https://github.com/qwc-services/qwc-admin-gui/raw/master/schemas/qwc-admin-gui.json',
                self.service_config('adminGui'), self.logger, 'admin-gui'),
            'dbAuth':
            ServiceConfig(
                'dbAuth',
                'https://github.com/qwc-services/qwc-db-auth/raw/master/schemas/qwc-db-auth.json',
                self.service_config('dbAuth'), self.logger, 'db-auth'),
            'elevation':
            ServiceConfig(
                'elevation',
                'https://github.com/qwc-services/qwc-elevation-service/raw/master/schemas/qwc-elevation-service.json',
                self.service_config('elevation'), self.logger),
            'mapinfo':
            ServiceConfig(
                'mapinfo',
                'https://github.com/qwc-services/qwc-mapinfo-service/raw/master/schemas/qwc-mapinfo-service.json',
                self.service_config('mapinfo'), self.logger),
            'permalink':
            ServiceConfig(
                'permalink',
                'https://github.com/qwc-services/qwc-permalink-service/raw/master/schemas/qwc-permalink-service.json',
                self.service_config('permalink'), self.logger)
        }

        try:
            # check tenant dirs
            if not os.path.isdir(self.temp_tenant_path):
                # create temp tenant dir
                self.logger.info("Creating temp tenant dir %s" %
                                 self.temp_tenant_path)
                os.mkdir(self.temp_tenant_path)

            if not os.path.isdir(self.tenant_path):
                # create tenant dir
                self.logger.info("Creating tenant dir %s" % self.tenant_path)
                os.mkdir(self.tenant_path)
        except Exception as e:
            self.logger.error("Could not create tenant dir:\n%s" % e)
Esempio n. 10
0
class ConfigGenerator():
    """ConfigGenerator class

    Generate JSON files for service configs and permissions
    from a themesConfig.json, WMS GetCapabilities and QWC ConfigDB.
    """
    def __init__(self, config, logger):
        """Constructor

        :param obj config: ConfigGenerator config
        :param Logger logger: Logger
        """
        self.logger = Logger(logger)

        self.config = config
        generator_config = config.get('config', {})
        self.tenant = generator_config.get('tenant', 'default')
        self.logger.info("Using tenant '%s'" % self.tenant)
        # Set output config path for the generated configuration files.
        # If `config_path` is not set in the configGeneratorConfig.json,
        # then either use the `OUTPUT_CONFIG_PATH` ENV variable (if it is set)
        # or default back to the `/tmp/` directory
        self.config_path = generator_config.get(
            'config_path', os.environ.get('OUTPUT_CONFIG_PATH', '/tmp/'))
        self.tenant_path = os.path.join(self.config_path, self.tenant)

        self.temp_config_path = tempfile.mkdtemp(prefix='qwc_')
        self.temp_tenant_path = os.path.join(self.temp_config_path,
                                             self.tenant)

        try:
            # load ORM models for ConfigDB
            config_db_url = generator_config.get(
                'config_db_url', 'postgresql:///?service=qwc_configdb')
            db_engine = DatabaseEngine()
            self.config_models = ConfigModels(db_engine, config_db_url)
        except Exception as e:
            msg = ("Could not load ConfigModels for ConfigDB at '%s':\n%s" %
                   (config_db_url, e))
            self.logger.error(msg)
            raise Exception(msg)

        # load capabilites for all QWC2 theme items
        self.capabilities_reader = CapabilitiesReader(
            generator_config, config.get("themesConfig"), self.logger)
        self.capabilities_reader.preprocess_qgs_projects(
            generator_config, self.tenant)
        self.capabilities_reader.search_qgs_projects(generator_config)
        self.capabilities_reader.load_all_project_settings()

        # lookup for additional service configs by name
        self.service_configs = {}
        for service_config in self.config.get('services', []):
            self.service_configs[service_config['name']] = service_config

        # create service config handlers
        self.config_handler = {
            # services with resources
            'ogc':
            OGCServiceConfig(generator_config,
                             self.capabilities_reader, self.config_models,
                             self.service_config('ogc'), self.logger),
            'mapViewer':
            MapViewerConfig(self.temp_tenant_path, generator_config,
                            self.capabilities_reader, self.config_models,
                            self.service_config('mapViewer'), self.logger),
            'featureInfo':
            FeatureInfoServiceConfig(generator_config,
                                     self.capabilities_reader,
                                     self.config_models,
                                     self.service_config('featureInfo'),
                                     self.logger),
            'print':
            PrintServiceConfig(self.capabilities_reader,
                               self.service_config('print'), self.logger),
            'search':
            SearchServiceConfig(self.config_models,
                                self.service_config('search'), self.logger),
            'legend':
            LegendServiceConfig(generator_config, self.capabilities_reader,
                                self.config_models,
                                self.service_config('legend'), self.logger),
            'data':
            DataServiceConfig(self.service_config('data'), generator_config,
                              self.config_models, self.logger),
            'ext':
            ExtServiceConfig(self.config_models, self.service_config('ext'),
                             self.logger),

            # config-only services
            'adminGui':
            ServiceConfig(
                'adminGui',
                'https://github.com/qwc-services/qwc-admin-gui/raw/master/schemas/qwc-admin-gui.json',
                self.service_config('adminGui'), self.logger, 'admin-gui'),
            'dbAuth':
            ServiceConfig(
                'dbAuth',
                'https://github.com/qwc-services/qwc-db-auth/raw/master/schemas/qwc-db-auth.json',
                self.service_config('dbAuth'), self.logger, 'db-auth'),
            'elevation':
            ServiceConfig(
                'elevation',
                'https://github.com/qwc-services/qwc-elevation-service/raw/master/schemas/qwc-elevation-service.json',
                self.service_config('elevation'), self.logger),
            'mapinfo':
            ServiceConfig(
                'mapinfo',
                'https://github.com/qwc-services/qwc-mapinfo-service/raw/master/schemas/qwc-mapinfo-service.json',
                self.service_config('mapinfo'), self.logger),
            'permalink':
            ServiceConfig(
                'permalink',
                'https://github.com/qwc-services/qwc-permalink-service/raw/master/schemas/qwc-permalink-service.json',
                self.service_config('permalink'), self.logger)
        }

        try:
            # check tenant dirs
            if not os.path.isdir(self.temp_tenant_path):
                # create temp tenant dir
                self.logger.info("Creating temp tenant dir %s" %
                                 self.temp_tenant_path)
                os.mkdir(self.temp_tenant_path)

            if not os.path.isdir(self.tenant_path):
                # create tenant dir
                self.logger.info("Creating tenant dir %s" % self.tenant_path)
                os.mkdir(self.tenant_path)
        except Exception as e:
            self.logger.error("Could not create tenant dir:\n%s" % e)

    def service_config(self, service):
        """Return any additional service config for service.

        :param str service: Service name
        """
        return self.service_configs.get(service, {})

    def write_configs(self):
        """Generate and save service config files.

        Return True if the config files could be generated.
        """
        for service_config in self.config.get('services', []):
            self.write_service_config(service_config['name'])

        for log in self.logger.log_entries():
            if log["level"] == self.logger.LEVEL_CRITICAL:
                self.logger.critical("The generation of the configuration"
                                     " files resulted in a failure")
                self.logger.critical(
                    "The configuration files were not updated!")
                return False

        for file_name in os.listdir(os.path.join(self.temp_tenant_path)):
            file_path = os.path.join(self.temp_tenant_path, file_name)
            if os.path.isfile(file_path):
                copyfile(file_path, os.path.join(self.tenant_path, file_name))

        self.logger.info(
            "The generation of the configuration files was successful")
        self.logger.info("Configuration files were updated!")
        return True

    def write_service_config(self, service):
        """Write service config file as JSON.

        :param str service: Service name
        """
        config_handler = self.config_handler.get(service)
        if config_handler:
            self.logger.info("Collecting '%s' service config" % service)

            # generate service config
            config = config_handler.config()

            # validate JSON schema
            if self.validate_schema(config, config_handler.schema):
                self.logger.info(
                    "'%s' service config validates against schema" % service)
            else:
                self.logger.error(
                    "'%s' service config failed schema validation" % service)

            # write service config file
            filename = '%sConfig.json' % config_handler.service_name
            self.logger.info("Writing '%s' service config file" % filename)
            self.write_json_file(config, filename)
        else:
            self.logger.warning("Service '%s' not found" % service)

    def write_permissions(self):
        """Generate and save service permissions.

        Return True if the service permissions could be generated.
        """
        permissions_config = PermissionsConfig(self.config_models, self.logger)
        permissions_query = PermissionsQuery(self.config_models, self.logger)
        permissions = permissions_config.base_config()

        # collect service permissions
        for service_config in self.config.get('services', []):
            service = service_config['name']
            config_handler = self.config_handler.get(service)
            if config_handler:
                self.logger.info("Collecting '%s' service permissions" %
                                 service)
                for role in permissions['roles']:
                    permissions_config.merge_service_permissions(
                        role['permissions'],
                        config_handler.permissions(role['role']))
            else:
                self.logger.warning("Service '%s' not found" % service)

        # write permissions for custom resources
        custom_resource_types = self.config.get('custom_resource_types', [])
        for resource_type in custom_resource_types:
            for role in permissions['roles']:

                res_permissions = OrderedDict()
                session = self.config_models.session()
                permitted_resources = permissions_query.permitted_resources
                resources = permitted_resources(resource_type, role['role'],
                                                session).keys()
                res_permissions[resource_type] = sorted(list(resources))
                session.close()

                permissions_config.merge_service_permissions(
                    role['permissions'], res_permissions)

        # validate JSON schema
        if self.validate_schema(permissions, permissions_config.schema):
            self.logger.info("Service permissions validate against schema")
        else:
            self.logger.error("Service permissions failed schema validation")

        self.logger.info("Writing 'permissions.json' permissions file")
        self.write_json_file(permissions, 'permissions.json')

        for log in self.logger.log_entries():
            if log["level"] == self.logger.LEVEL_CRITICAL:
                self.logger.critical("The generation of the permission"
                                     " files resulted in a failure.")
                self.logger.critical("The permission files were not updated!")
                return False

        copyfile(os.path.join(self.temp_tenant_path, 'permissions.json'),
                 os.path.join(self.tenant_path, 'permissions.json'))
        self.logger.info(
            "The generation of the permission files was successful")
        self.logger.info("permission files were updated!")
        return True

    def write_json_file(self, config, filename):
        """Write config to JSON file in config path.

        :param OrderedDict config: Config data
        """
        try:
            path = os.path.join(self.temp_tenant_path, filename)
            with open(path, 'wb') as f:
                # NOTE: keep order of keys
                f.write(
                    json.dumps(config,
                               sort_keys=False,
                               ensure_ascii=False,
                               indent=2).encode('utf8'))
        except Exception as e:
            self.logger.error("Could not write '%s' config file:\n%s" %
                              (filename, e))

    def cleanup_temp_dir(self):
        """Remove temporary config dir."""
        try:
            if os.path.isdir(self.temp_config_path):
                self.logger.debug("Removing temp config dir %s" %
                                  self.temp_config_path)
                rmtree(self.temp_config_path)
        except Exception as e:
            self.logger.error("Could not remove temp config dir:\n%s" % e)

    def validate_schema(self, config, schema_url):
        """Validate config against its JSON schema.

        :param OrderedDict config: Config data
        :param str schema_url: JSON schema URL
        """
        # download JSON schema
        response = requests.get(schema_url)
        if response.status_code != requests.codes.ok:
            self.logger.error("Could not download JSON schema from %s:\n%s" %
                              (schema_url, response.text))
            return False

        # parse JSON
        try:
            schema = json.loads(response.text)
        except Exception as e:
            self.logger.error("Could not parse JSON schema:\n%s" % e)
            return False

        # FIXME: remove external schema refs from MapViewer schema for now
        #        until QWC2 JSON schemas are available
        if config.get('service') == 'map-viewer':
            self.logger.info("Skipping JSON schema check for MapViewer")
            resources = schema['properties']['resources']['properties']
            # QWC2 application configuration as simple dict
            resources['qwc2_config']['properties']['config'] = {
                'type': 'object'
            }
            # QWC2 themes configuration as simple dict with 'themes'
            resources['qwc2_themes'] = {
                'type': 'object',
                'properties': {
                    'themes': {
                        'type': 'object'
                    }
                },
                'required': ['themes']
            }

        # validate against schema
        valid = True
        validator = jsonschema.validators.validator_for(schema)(schema)
        for error in validator.iter_errors(config):
            valid = False

            # collect error messages
            messages = [e.message for e in error.context]
            if not messages:
                messages = [error.message]

            # collect path to concerned subconfig
            # e.g. ['resources', 'wms_services', 0]
            #      => ".resources.wms_services[0]"
            path = ""
            for p in error.absolute_path:
                if isinstance(p, int):
                    path += "[%d]" % p
                else:
                    path += ".%s" % p

            # get concerned subconfig
            instance = error.instance
            if isinstance(error.instance, dict):
                # get first level of properties of concerned subconfig
                instance = OrderedDict()
                for key, value in error.instance.items():
                    if isinstance(value, dict) and value.keys():
                        first_value_key = list(value.keys())[0]
                        instance[key] = {first_value_key: '...'}
                    elif isinstance(value, list):
                        instance[key] = ['...']
                    else:
                        instance[key] = value

            # log errors
            message = ""
            if len(messages) == 1:
                message = "Validation error: %s" % messages[0]
            else:
                message = "\nValidation errors:\n"
                for msg in messages:
                    message += "  * %s\n" % msg
            self.logger.error(message)
            self.logger.warning("Location: %s" % path)
            self.logger.warning("Value: %s" % json.dumps(
                instance, sort_keys=False, indent=2, ensure_ascii=False))

        return valid

    def get_logger(self):
        return self.logger

    def maps(self):
        """Return list of map names from QWC2 theme items."""
        return self.capabilities_reader.wms_service_names()

    def map_details(self, map_name, with_attributes=False):
        """Return details for a map from capabilities

        :param str map_name: Map name
        """
        map_details = OrderedDict()
        map_details['map'] = map_name
        map_details['layers'] = []

        # find map in capabilities
        cap = self.capabilities_reader.wms_capabilities.get(map_name)
        if cap is None:
            map_details['error'] = "Map not found"
        else:
            # collect list of layer names
            root_layer = cap.get('root_layer', {})
            if with_attributes is False:
                map_details['layers'] = self.collect_layer_names(root_layer)
            else:
                map_details['layers'] = self.collect_layers(root_layer)
                for print_layer in cap.get('internal_print_layers', []):
                    map_details['layers'].append({print_layer: []})

                for geometryless_layer in cap.get('geometryless_layers', []):
                    map_details['layers'].append({geometryless_layer: []})

        return map_details

    def collect_layer_names(self, layer):
        """Recursively collect list of layer names from capabilities.

        :param obj layer: Layer or group layer
        """
        layers = []

        layers.append(layer['name'])
        if 'layers' in layer:
            # group layer
            for sublayer in layer['layers']:
                layers += self.collect_layer_names(sublayer)

        return layers

    def collect_layers(self, layer):
        """Recursively collect list of layer names from capabilities.

        :param obj layer: Layer or group layer
        """
        # dict containing all layers and atrributes of a map
        layers = []

        if 'attributes' in layer:
            layers.append({layer['name']: layer['attributes']})
        elif 'layers' in layer:
            # group layer
            layers.append({layer['name']: []})
            for sublayer in layer['layers']:
                layers += self.collect_layers(sublayer)

        return layers
Esempio n. 11
0
    def __init__(self, config, logger):
        """Constructor

        :param obj config: ConfigGenerator config
        :param Logger logger: Logger
        """
        self.logger = Logger(logger)

        self.config = config
        generator_config = config.get('config', {})
        self.tenant = generator_config.get('tenant', 'default')
        self.logger.debug("Using tenant '%s'" % self.tenant)

        # get default QGIS server URL from ConfigGenerator config
        self.default_qgis_server_url = generator_config.get(
            'default_qgis_server_url',
            'http://localhost:8001/ows/').rstrip('/') + '/'

        # Set output config path for the generated configuration files.
        # If `config_path` is not set in the configGeneratorConfig.json,
        # then either use the `OUTPUT_CONFIG_PATH` ENV variable (if it is set)
        # or default back to the `/tmp/` directory
        self.config_path = generator_config.get(
            'config_path', os.environ.get('OUTPUT_CONFIG_PATH', '/tmp/'))
        self.tenant_path = os.path.join(self.config_path, self.tenant)
        self.logger.info("Config destination: '%s'" % self.tenant_path)

        self.temp_config_path = tempfile.mkdtemp(prefix='qwc_')
        self.temp_tenant_path = os.path.join(self.temp_config_path,
                                             self.tenant)

        self.do_validate_schema = str(
            generator_config.get('validate_schema', True)).lower() != 'false'

        try:
            # load ORM models for ConfigDB
            config_db_url = generator_config.get(
                'config_db_url', 'postgresql:///?service=qwc_configdb')
            db_engine = DatabaseEngine()
            self.config_models = ConfigModels(db_engine, config_db_url)
        except Exception as e:
            msg = ("Could not load ConfigModels for ConfigDB at '%s':\n%s" %
                   (config_db_url, e))
            self.logger.error(msg)
            raise Exception(msg)

        themes_config = config.get("themesConfig", {})

        # Preprocess QGS projects
        self.preprocess_qgs_projects(generator_config, self.tenant)

        # Search for QGS projects in scan dir and automatically generate theme items
        self.search_qgs_projects(generator_config, themes_config)

        # load metadata for all QWC2 theme items
        self.theme_reader = ThemeReader(generator_config, themes_config,
                                        self.logger)

        # lookup for additional service configs by name
        self.service_configs = {}
        for service_config in self.config.get('services', []):
            self.service_configs[service_config['name']] = service_config

        # load schema-versions.json
        schema_versions = {}
        schema_versions_path = os.path.join(
            os.path.dirname(os.path.realpath(__file__)),
            '../schemas/schema-versions.json')
        try:
            with open(schema_versions_path) as f:
                schema_versions = json.load(f)
        except Exception as e:
            msg = ("Could not load JSON schema versions from %s:\n%s" %
                   (schema_versions_path, e))
            self.logger.error(msg)
            raise Exception(msg)

        # lookup for JSON schema URLs by service name
        self.schema_urls = {}
        for schema in schema_versions.get('schemas', []):
            self.schema_urls[schema.get('service')] = schema.get(
                'schema_url', '')

        # get path to downloaded JSON schema files
        self.json_schemas_path = os.environ.get('JSON_SCHEMAS_PATH', '/tmp/')

        # create service config handlers
        self.config_handler = {
            # services with resources
            'ogc':
            OGCServiceConfig(generator_config, self.theme_reader,
                             self.config_models, self.schema_urls.get('ogc'),
                             self.service_config('ogc'), self.logger),
            'mapViewer':
            MapViewerConfig(self.temp_tenant_path, generator_config,
                            self.theme_reader, self.config_models,
                            self.schema_urls.get('mapViewer'),
                            self.service_config('mapViewer'), self.logger),
            'featureInfo':
            FeatureInfoServiceConfig(generator_config, self.theme_reader,
                                     self.config_models,
                                     self.schema_urls.get('featureInfo'),
                                     self.service_config('featureInfo'),
                                     self.logger),
            'print':
            PrintServiceConfig(self.theme_reader,
                               self.schema_urls.get('print'),
                               self.service_config('print'), self.logger),
            'search':
            SearchServiceConfig(self.config_models,
                                self.schema_urls.get('search'),
                                self.service_config('search'), self.logger),
            'legend':
            LegendServiceConfig(generator_config, self.theme_reader,
                                self.config_models,
                                self.schema_urls.get('legend'),
                                self.service_config('legend'), self.logger),
            'data':
            DataServiceConfig(generator_config, self.theme_reader,
                              self.config_models, self.schema_urls.get('data'),
                              self.service_config('data'), self.logger),
            'ext':
            ExtServiceConfig(self.config_models, self.schema_urls.get('ext'),
                             self.service_config('ext'), self.logger),

            # config-only services
            'adminGui':
            ServiceConfig('adminGui', self.schema_urls.get('adminGui'),
                          self.service_config('adminGui'), self.logger,
                          'admin-gui'),
            'dbAuth':
            ServiceConfig('dbAuth', self.schema_urls.get('dbAuth'),
                          self.service_config('dbAuth'), self.logger,
                          'db-auth'),
            'elevation':
            ServiceConfig('elevation', self.schema_urls.get('elevation'),
                          self.service_config('elevation'), self.logger),
            'mapinfo':
            ServiceConfig('mapinfo', self.schema_urls.get('mapinfo'),
                          self.service_config('mapinfo'), self.logger),
            'permalink':
            ServiceConfig('permalink', self.schema_urls.get('permalink'),
                          self.service_config('permalink'), self.logger)
        }

        try:
            # check tenant dirs
            if not os.path.isdir(self.temp_tenant_path):
                # create temp tenant dir
                self.logger.debug("Creating temp tenant dir %s" %
                                  self.temp_tenant_path)
                os.mkdir(self.temp_tenant_path)

            if not os.path.isdir(self.tenant_path):
                # create tenant dir
                self.logger.info("Creating tenant dir %s" % self.tenant_path)
                os.mkdir(self.tenant_path)
        except Exception as e:
            self.logger.error("Could not create tenant dir:\n%s" % e)
Esempio n. 12
0
class ConfigGenerator():
    """ConfigGenerator class

    Generate JSON files for service configs and permissions
    from a tenantConfig.json and QWC ConfigDB.
    """
    def __init__(self, config, logger):
        """Constructor

        :param obj config: ConfigGenerator config
        :param Logger logger: Logger
        """
        self.logger = Logger(logger)

        self.config = config
        generator_config = config.get('config', {})
        self.tenant = generator_config.get('tenant', 'default')
        self.logger.debug("Using tenant '%s'" % self.tenant)

        # get default QGIS server URL from ConfigGenerator config
        self.default_qgis_server_url = generator_config.get(
            'default_qgis_server_url',
            'http://localhost:8001/ows/').rstrip('/') + '/'

        # Set output config path for the generated configuration files.
        # If `config_path` is not set in the configGeneratorConfig.json,
        # then either use the `OUTPUT_CONFIG_PATH` ENV variable (if it is set)
        # or default back to the `/tmp/` directory
        self.config_path = generator_config.get(
            'config_path', os.environ.get('OUTPUT_CONFIG_PATH', '/tmp/'))
        self.tenant_path = os.path.join(self.config_path, self.tenant)
        self.logger.info("Config destination: '%s'" % self.tenant_path)

        self.temp_config_path = tempfile.mkdtemp(prefix='qwc_')
        self.temp_tenant_path = os.path.join(self.temp_config_path,
                                             self.tenant)

        self.do_validate_schema = str(
            generator_config.get('validate_schema', True)).lower() != 'false'

        try:
            # load ORM models for ConfigDB
            config_db_url = generator_config.get(
                'config_db_url', 'postgresql:///?service=qwc_configdb')
            db_engine = DatabaseEngine()
            self.config_models = ConfigModels(db_engine, config_db_url)
        except Exception as e:
            msg = ("Could not load ConfigModels for ConfigDB at '%s':\n%s" %
                   (config_db_url, e))
            self.logger.error(msg)
            raise Exception(msg)

        themes_config = config.get("themesConfig", {})

        # Preprocess QGS projects
        self.preprocess_qgs_projects(generator_config, self.tenant)

        # Search for QGS projects in scan dir and automatically generate theme items
        self.search_qgs_projects(generator_config, themes_config)

        # load metadata for all QWC2 theme items
        self.theme_reader = ThemeReader(generator_config, themes_config,
                                        self.logger)

        # lookup for additional service configs by name
        self.service_configs = {}
        for service_config in self.config.get('services', []):
            self.service_configs[service_config['name']] = service_config

        # load schema-versions.json
        schema_versions = {}
        schema_versions_path = os.path.join(
            os.path.dirname(os.path.realpath(__file__)),
            '../schemas/schema-versions.json')
        try:
            with open(schema_versions_path) as f:
                schema_versions = json.load(f)
        except Exception as e:
            msg = ("Could not load JSON schema versions from %s:\n%s" %
                   (schema_versions_path, e))
            self.logger.error(msg)
            raise Exception(msg)

        # lookup for JSON schema URLs by service name
        self.schema_urls = {}
        for schema in schema_versions.get('schemas', []):
            self.schema_urls[schema.get('service')] = schema.get(
                'schema_url', '')

        # get path to downloaded JSON schema files
        self.json_schemas_path = os.environ.get('JSON_SCHEMAS_PATH', '/tmp/')

        # create service config handlers
        self.config_handler = {
            # services with resources
            'ogc':
            OGCServiceConfig(generator_config, self.theme_reader,
                             self.config_models, self.schema_urls.get('ogc'),
                             self.service_config('ogc'), self.logger),
            'mapViewer':
            MapViewerConfig(self.temp_tenant_path, generator_config,
                            self.theme_reader, self.config_models,
                            self.schema_urls.get('mapViewer'),
                            self.service_config('mapViewer'), self.logger),
            'featureInfo':
            FeatureInfoServiceConfig(generator_config, self.theme_reader,
                                     self.config_models,
                                     self.schema_urls.get('featureInfo'),
                                     self.service_config('featureInfo'),
                                     self.logger),
            'print':
            PrintServiceConfig(self.theme_reader,
                               self.schema_urls.get('print'),
                               self.service_config('print'), self.logger),
            'search':
            SearchServiceConfig(self.config_models,
                                self.schema_urls.get('search'),
                                self.service_config('search'), self.logger),
            'legend':
            LegendServiceConfig(generator_config, self.theme_reader,
                                self.config_models,
                                self.schema_urls.get('legend'),
                                self.service_config('legend'), self.logger),
            'data':
            DataServiceConfig(generator_config, self.theme_reader,
                              self.config_models, self.schema_urls.get('data'),
                              self.service_config('data'), self.logger),
            'ext':
            ExtServiceConfig(self.config_models, self.schema_urls.get('ext'),
                             self.service_config('ext'), self.logger),

            # config-only services
            'adminGui':
            ServiceConfig('adminGui', self.schema_urls.get('adminGui'),
                          self.service_config('adminGui'), self.logger,
                          'admin-gui'),
            'dbAuth':
            ServiceConfig('dbAuth', self.schema_urls.get('dbAuth'),
                          self.service_config('dbAuth'), self.logger,
                          'db-auth'),
            'elevation':
            ServiceConfig('elevation', self.schema_urls.get('elevation'),
                          self.service_config('elevation'), self.logger),
            'mapinfo':
            ServiceConfig('mapinfo', self.schema_urls.get('mapinfo'),
                          self.service_config('mapinfo'), self.logger),
            'permalink':
            ServiceConfig('permalink', self.schema_urls.get('permalink'),
                          self.service_config('permalink'), self.logger)
        }

        try:
            # check tenant dirs
            if not os.path.isdir(self.temp_tenant_path):
                # create temp tenant dir
                self.logger.debug("Creating temp tenant dir %s" %
                                  self.temp_tenant_path)
                os.mkdir(self.temp_tenant_path)

            if not os.path.isdir(self.tenant_path):
                # create tenant dir
                self.logger.info("Creating tenant dir %s" % self.tenant_path)
                os.mkdir(self.tenant_path)
        except Exception as e:
            self.logger.error("Could not create tenant dir:\n%s" % e)

    def service_config(self, service):
        """Return any additional service config for service.

        :param str service: Service name
        """
        return self.service_configs.get(service, {})

    def write_configs(self):
        """Generate and save service config files.

        Return True if the config files could be generated.
        """
        for service_config in self.config.get('services', []):
            self.write_service_config(service_config['name'])

        for log in self.logger.log_entries():
            if log["level"] == self.logger.LEVEL_CRITICAL:
                self.logger.critical("The generation of the configuration"
                                     " files resulted in a failure")
                self.logger.critical(
                    "The configuration files were not updated!")
                return False

        for file_name in os.listdir(os.path.join(self.temp_tenant_path)):
            file_path = os.path.join(self.temp_tenant_path, file_name)
            if os.path.isfile(file_path):
                copyfile(file_path, os.path.join(self.tenant_path, file_name))

        self.logger.info(
            "The generation of the configuration files was successful")
        self.logger.info("Configuration files were updated!")
        return True

    def write_service_config(self, service):
        """Write service config file as JSON.

        :param str service: Service name
        """
        config_handler = self.config_handler.get(service)
        if config_handler:
            self.logger.debug("Collecting '%s' service config" % service)

            # generate service config
            config = config_handler.config()

            # validate JSON schema
            if self.validate_schema(config, config_handler.schema):
                self.logger.debug(
                    "'%s' service config validates against schema" % service)
            else:
                self.logger.error(
                    "'%s' service config failed schema validation" % service)

            # write service config file
            filename = '%sConfig.json' % config_handler.service_name
            self.logger.info("Writing '%s' service config file" % filename)
            self.write_json_file(config, filename)
        else:
            self.logger.warning("Service '%s' not found" % service)

    def write_permissions(self):
        """Generate and save service permissions.

        Return True if the service permissions could be generated.
        """
        permissions_config = PermissionsConfig(
            self.config_models, self.schema_urls.get('permissions'),
            self.logger)
        permissions_query = PermissionsQuery(self.config_models, self.logger)
        permissions = permissions_config.base_config()

        # collect service permissions
        for service_config in self.config.get('services', []):
            service = service_config['name']
            config_handler = self.config_handler.get(service)
            if config_handler:
                self.logger.info("Collecting '%s' service permissions" %
                                 service)
                for role in permissions['roles']:
                    permissions_config.merge_service_permissions(
                        role['permissions'],
                        config_handler.permissions(role['role']))
            else:
                self.logger.warning("Service '%s' not found" % service)

        # write permissions for custom resources
        custom_resource_types = self.config.get('custom_resource_types', [])
        for resource_type in custom_resource_types:
            for role in permissions['roles']:

                res_permissions = OrderedDict()
                session = self.config_models.session()
                permitted_resources = permissions_query.permitted_resources
                resources = permitted_resources(resource_type, role['role'],
                                                session).keys()
                res_permissions[resource_type] = sorted(list(resources))
                session.close()

                permissions_config.merge_service_permissions(
                    role['permissions'], res_permissions)

        # validate JSON schema
        if self.validate_schema(permissions, permissions_config.schema):
            self.logger.debug("Service permissions validate against schema")
        else:
            self.logger.error("Service permissions failed schema validation")

        self.logger.info("Writing 'permissions.json' permissions file")
        self.write_json_file(permissions, 'permissions.json')

        for log in self.logger.log_entries():
            if log["level"] == self.logger.LEVEL_CRITICAL:
                self.logger.critical("The generation of the permission"
                                     " files resulted in a failure.")
                self.logger.critical("The permission files were not updated!")
                return False

        copyfile(os.path.join(self.temp_tenant_path, 'permissions.json'),
                 os.path.join(self.tenant_path, 'permissions.json'))
        self.logger.info(
            "The generation of the permission files was successful")
        self.logger.info("permission files were updated!")
        return True

    def write_json_file(self, config, filename):
        """Write config to JSON file in config path.

        :param OrderedDict config: Config data
        """
        try:
            path = os.path.join(self.temp_tenant_path, filename)
            with open(path, 'wb') as f:
                # NOTE: keep order of keys
                f.write(
                    json.dumps(config,
                               sort_keys=False,
                               ensure_ascii=False,
                               indent=2).encode('utf8'))
        except Exception as e:
            self.logger.error("Could not write '%s' config file:\n%s" %
                              (filename, e))

    def cleanup_temp_dir(self):
        """Remove temporary config dir."""
        try:
            if os.path.isdir(self.temp_config_path):
                self.logger.debug("Removing temp config dir %s" %
                                  self.temp_config_path)
                rmtree(self.temp_config_path)
        except Exception as e:
            self.logger.error("Could not remove temp config dir:\n%s" % e)

    def validate_schema(self, config, schema_url):
        """Validate config against its JSON schema.

        :param OrderedDict config: Config data
        :param str schema_url: JSON schema URL
        """
        if not self.do_validate_schema:
            self.logger.debug("Skipping schema validation")
            return True

        # load local JSON schema file
        schema = None
        try:
            # parse schema URL
            file_name = os.path.basename(urlparse(schema_url).path)
            file_path = os.path.join(self.json_schemas_path, file_name)
            with open(file_path) as f:
                schema = json.load(f)
        except Exception as e:
            self.logger.warning("Could not load JSON schema from %s:\n%s" %
                                (file_path, e))

        if not schema:
            # download JSON schema
            self.logger.info("Downloading JSON schema from %s" % schema_url)
            try:
                response = requests.get(schema_url)
            except Exception as e:
                self.logger.error(
                    "Could not download JSON schema from %s:\n%s" %
                    (schema_url, str(e)))
                return False

            if response.status_code != requests.codes.ok:
                self.logger.error(
                    "Could not download JSON schema from %s:\n%s" %
                    (schema_url, response.text))
                return False

            # parse JSON
            try:
                schema = json.loads(response.text)
            except Exception as e:
                self.logger.error("Could not parse JSON schema:\n%s" % e)
                return False

        # validate against schema
        valid = True
        validator = jsonschema.validators.validator_for(schema)(schema)
        for error in validator.iter_errors(config):
            valid = False

            # collect error messages
            messages = [e.message for e in error.context]
            if not messages:
                messages = [error.message]

            # collect path to concerned subconfig
            # e.g. ['resources', 'wms_services', 0]
            #      => ".resources.wms_services[0]"
            path = ""
            for p in error.absolute_path:
                if isinstance(p, int):
                    path += "[%d]" % p
                else:
                    path += ".%s" % p

            # get concerned subconfig
            instance = error.instance
            if isinstance(error.instance, dict):
                # get first level of properties of concerned subconfig
                instance = OrderedDict()
                for key, value in error.instance.items():
                    if isinstance(value, dict) and value.keys():
                        first_value_key = list(value.keys())[0]
                        instance[key] = {first_value_key: '...'}
                    elif isinstance(value, list):
                        instance[key] = ['...']
                    else:
                        instance[key] = value

            # log errors
            message = ""
            if len(messages) == 1:
                message = "Validation error: %s" % messages[0]
            else:
                message = "\nValidation errors:\n"
                for msg in messages:
                    message += "  * %s\n" % msg
            self.logger.error(message)
            self.logger.warning("Location: %s" % path)
            self.logger.warning("Value: %s" % json.dumps(
                instance, sort_keys=False, indent=2, ensure_ascii=False))

        return valid

    def preprocess_qgs_projects(self, generator_config, tenant):
        config_in_path = os.environ.get('INPUT_CONFIG_PATH', 'config-in/')

        if os.path.exists(config_in_path) is False:
            self.logger.warning("The specified path does not exist: " +
                                config_in_path)
            return

        qgs_projects_dir = os.path.join(config_in_path, tenant,
                                        "qgis_projects")
        if os.path.exists(qgs_projects_dir):
            self.logger.info("Searching for projects files in " +
                             qgs_projects_dir)
        else:
            self.logger.debug(
                "The qgis_projects sub directory does not exist: " +
                qgs_projects_dir)
            return

        # Output directory for processed projects
        qgis_projects_gen_base_dir = generator_config.get(
            'qgis_projects_gen_base_dir')
        if not qgis_projects_gen_base_dir:
            self.logger.warning("Skipping preprocessing qgis projects in " +
                                qgs_projects_dir +
                                ": qgis_projects_gen_base_dir is not set")
            return

        for dirpath, dirs, files in os.walk(qgs_projects_dir,
                                            followlinks=True):
            for filename in files:
                if Path(filename).suffix in [".qgs", ".qgz"]:
                    fname = os.path.join(dirpath, filename)
                    relpath = os.path.relpath(fname, qgs_projects_dir)
                    self.logger.info("Processing " + fname)

                    # convert project
                    dest_path = os.path.join(qgis_projects_gen_base_dir,
                                             relpath)

                    if generator_config.get('split_categorized_layers',
                                            False) is True:
                        from .categorize_groups_script import split_categorized_layers
                        split_categorized_layers(fname, dest_path)
                    else:
                        copyfile(fname, dest_path)
                    if not os.path.exists(dest_path):
                        self.logger.warning(
                            "The project: " + dest_path +
                            " could not be generated.\n"
                            "Please check if needed permissions to create the"
                            " file are granted.")
                        continue
                    self.logger.info("Written to " + dest_path)

    def search_qgs_projects(self, generator_config, themes_config):

        qgis_projects_base_dir = generator_config.get('qgis_projects_base_dir')
        qgis_projects_scan_base_dir = generator_config.get(
            'qgis_projects_scan_base_dir')
        qwc_base_dir = generator_config.get("qwc2_base_dir")

        if not qgis_projects_scan_base_dir:
            self.logger.info("Skipping scanning for projects" +
                             " (qgis_projects_scan_base_dir not set)")
            return

        if os.path.exists(qgis_projects_scan_base_dir):
            self.logger.info("Searching for projects files in " +
                             qgis_projects_scan_base_dir)
        else:
            self.logger.error("The qgis_projects_scan_base_dir sub directory" +
                              " does not exist: " +
                              qgis_projects_scan_base_dir)
            return

        # collect existing item urls
        items = themes_config.get("themes", {}).get("items", {})
        wms_urls = []
        has_default = False
        for item in items:
            if item.get("url"):
                wms_urls.append(item["url"])
            if item.get("default", False):
                has_default = True

        # This is needed because we don't want to
        # print the error message "thumbnail dir not found"
        # multiple times
        thumbnail_dir_exists = True
        thumbnail_directory = ""
        if qwc_base_dir is None:
            thumbnail_dir_exists = False
            self.logger.info("Skipping automatic thumbnail search "
                             "(qwc2_base_dir was not set)")
        else:
            thumbnail_directory = os.path.join(qwc_base_dir,
                                               "assets/img/mapthumbs")

        for dirpath, dirs, files in os.walk(qgis_projects_scan_base_dir,
                                            followlinks=True):
            for filename in files:
                if Path(filename).suffix in [".qgs", ".qgz"]:
                    fname = os.path.join(dirpath, filename)
                    relpath = os.path.relpath(dirpath, qgis_projects_base_dir)
                    wmspath = os.path.join(relpath, Path(filename).stem)
                    wmsurlpath = urlparse(
                        urljoin(self.default_qgis_server_url, wmspath)).path

                    # Add to themes items
                    item = OrderedDict()
                    item["url"] = wmsurlpath
                    item["backgroundLayers"] = themes_config.get(
                        "defaultBackgroundLayers", [])
                    item["searchProviders"] = themes_config.get(
                        "defaultSearchProviders", [])
                    item["mapCrs"] = themes_config.get("defaultMapCrs")

                    # Check if thumbnail directory exists
                    if thumbnail_dir_exists and not os.path.exists(
                            thumbnail_directory):
                        self.logger.info(
                            "Thumbnail directory: %s does not exist" %
                            (thumbnail_directory))
                        thumbnail_dir_exists = False

                    # Scanning for thumbnail
                    if thumbnail_dir_exists:
                        thumbnail_filename = "%s.png" % Path(filename).stem
                        self.logger.info(
                            "Scanning for thumbnail(%s) under %s" %
                            (thumbnail_filename, thumbnail_directory))
                        thumbnail_path = os.path.join(thumbnail_directory,
                                                      thumbnail_filename)

                        if os.path.exists(thumbnail_path):
                            self.logger.info("Thumbnail: %s was found" %
                                             (thumbnail_filename))
                            item["thumbnail"] = thumbnail_filename
                        else:
                            self.logger.info(
                                "Thumbnail: %s could not be found under %s" %
                                (thumbnail_filename, thumbnail_path))

                    if item["url"] not in wms_urls:
                        self.logger.info("Adding project " + fname)
                        if not has_default:
                            item["default"] = True
                            has_default = True
                        items.append(item)
                    else:
                        self.logger.info("Skipping project " + fname)

    def get_logger(self):
        return self.logger

    def maps(self):
        """Return list of map names from QWC2 theme items."""
        return self.theme_reader.wms_service_names()

    def map_details(self, map_name, with_attributes=False):
        """Return details for a map from capabilities

        :param str map_name: Map name
        """
        map_details = OrderedDict()
        map_details['map'] = map_name
        map_details['layers'] = []

        # find map in capabilities
        theme_metadata = self.theme_reader.theme_metadata.get(map_name)
        if theme_metadata is None:
            map_details['error'] = "Map not found"
        else:
            cap = theme_metadata['wms_capabilities']
            # collect list of layer names
            root_layer = cap.get('root_layer', {})
            if with_attributes is False:
                map_details['layers'] = self.collect_layer_names(root_layer)
            else:
                map_details['layers'] = self.collect_layers(root_layer)
                for print_layer in cap.get('internal_print_layers', []):
                    map_details['layers'].append({print_layer: []})

                for geometryless_layer in cap.get('geometryless_layers', []):
                    map_details['layers'].append({geometryless_layer: []})

        return map_details

    def collect_layer_names(self, layer):
        """Recursively collect list of layer names from capabilities.

        :param obj layer: Layer or group layer
        """
        layers = []

        layers.append(layer['name'])
        if 'layers' in layer:
            # group layer
            for sublayer in layer['layers']:
                layers += self.collect_layer_names(sublayer)

        return layers

    def collect_layers(self, layer):
        """Recursively collect list of layer names from capabilities.

        :param obj layer: Layer or group layer
        """
        # dict containing all layers and atrributes of a map
        layers = []

        if 'attributes' in layer:
            layers.append({layer['name']: layer['attributes']})
        elif 'layers' in layer:
            # group layer
            layers.append({layer['name']: []})
            for sublayer in layer['layers']:
                layers += self.collect_layers(sublayer)

        return layers
Esempio n. 13
0
class AccessControl:

    # name of admin iam.role
    ADMIN_ROLE_NAME = 'admin'

    def __init__(self, handler, logger):
        """Constructor

        :param ConfigModels handler: Helper for ORM models
        :param Logger logger: Application logger
        """
        self.handler = handler
        self.logger = logger

    def is_admin(self, identity):
        db_engine = self.handler().db_engine()
        self.config_models = ConfigModels(db_engine, self.handler().conn_str())

        # Extract user infos from identity
        if isinstance(identity, dict):
            username = identity.get('username')
            group = identity.get('group')
        else:
            username = identity
            group = None
        session = self.config_models.session()
        admin_role = self.admin_role_query(username, group, session)
        session.close()

        return admin_role

    def admin_role_query(self, username, group, session):
        """Create base query for all permissions of a user and group.

        Combine permissions from roles of user and user groups, group roles and
        public role.

        :param str username: User name
        :param str group: Group name
        :param Session session: DB session
        """
        Role = self.config_models.model('roles')
        Group = self.config_models.model('groups')
        User = self.config_models.model('users')

        # create query
        query = session.query(Role)

        # query permissions from roles in user groups
        groups_roles_query = query.join(Role.groups_collection) \
            .join(Group.users_collection) \
            .filter(User.name == username)

        # query permissions from direct user roles
        user_roles_query = query.join(Role.users_collection) \
            .filter(User.name == username)

        # query permissions from group roles
        group_roles_query = query.join(Role.groups_collection) \
            .filter(Group.name == group)

        # combine queries
        query = groups_roles_query.union(user_roles_query) \
            .union(group_roles_query) \
            .filter(Role.name == self.ADMIN_ROLE_NAME)

        (admin_role, ), = session.query(query.exists())

        return admin_role
Esempio n. 14
0
    def __init__(self, app, handler, themesconfig):
        """Constructor

        :param Flask app: Flask application
        """

        # index
        app.add_url_rule(
            "/themes", "themes", self.index, methods=["GET"]
        )
        # new
        app.add_url_rule(
            "/themes/new", "new_theme", self.new_theme,
            methods=["GET"]
        )
        app.add_url_rule(
            "/themes/new/<int:gid>", "new_theme", self.new_theme,
            methods=["GET"]
        )
        # create
        app.add_url_rule(
            "/themes/create", "create_theme", self.create_theme,
            methods=["POST"]
        )
        app.add_url_rule(
            "/themes/create/<int:gid>", "create_theme", self.create_theme,
            methods=["POST"]
        )
        # edit
        app.add_url_rule(
            "/themes/edit/<int:tid>", "edit_theme", self.edit_theme,
            methods=["GET"]
        )
        app.add_url_rule(
            "/themes/edit/<int:tid>/<int:gid>", "edit_theme", self.edit_theme,
            methods=["GET"]
        )
        # update
        app.add_url_rule(
            "/themes/update/<int:tid>", "update_theme",
            self.update_theme, methods=["POST"]
        )
        app.add_url_rule(
            "/themes/update/<int:tid>/<int:gid>", "update_theme",
            self.update_theme, methods=["POST"]
        )
        # delete
        app.add_url_rule(
            "/themes/delete/<int:tid>", "delete_theme",
            self.delete_theme, methods=["GET"]
        )
        app.add_url_rule(
            "/themes/delete/<int:tid>/<int:gid>", "delete_theme",
            self.delete_theme, methods=["GET"]
        )
        # move
        app.add_url_rule(
            "/themes/move/<string:direction>/<int:tid>",
            "move_theme", self.move_theme, methods=["GET"]
        )
        app.add_url_rule(
            "/themes/move/<string:direction>/<int:tid>/<int:gid>",
            "move_theme", self.move_theme, methods=["GET"]
        )

        # add group
        app.add_url_rule(
            "/themes/add_theme_group", "add_theme_group", self.add_theme_group,
            methods=["GET"]
        )
        # delete group
        app.add_url_rule(
            "/themes/delete_theme_group/<int:gid>", "delete_theme_group",
            self.delete_theme_group, methods=["GET"]
        )
        # update group
        app.add_url_rule(
            "/themes/update_theme_group/<int:gid>", "update_theme_group",
            self.update_theme_group, methods=["POST"]
        )
        # move group
        app.add_url_rule(
            "/themes/move_theme_group/<string:direction>/<int:gid>",
            "move_theme_group", self.move_theme_group, methods=["GET"]
        )

        # save themesconfig
        app.add_url_rule(
            "/themes/save_themesconfig", "save_themesconfig",
            self.save_themesconfig, methods=["GET"]
        )
        # reset themesconfig
        app.add_url_rule(
            "/themes/reset_themesconfig", "reset_themesconfig",
            self.reset_themesconfig, methods=["GET"]
        )

        self.app = app
        self.handler = handler
        self.themesconfig = themesconfig
        self.template_dir = "plugins/themes/templates"

        config_handler = handler()
        db_engine = config_handler.db_engine()
        self.config_models = ConfigModels(db_engine, config_handler.conn_str())
        self.resources = self.config_models.model('resources')
Esempio n. 15
0
class ThemesController:
    """Controller for theme model"""

    def __init__(self, app, handler, themesconfig):
        """Constructor

        :param Flask app: Flask application
        """

        # index
        app.add_url_rule(
            "/themes", "themes", self.index, methods=["GET"]
        )
        # new
        app.add_url_rule(
            "/themes/new", "new_theme", self.new_theme,
            methods=["GET"]
        )
        app.add_url_rule(
            "/themes/new/<int:gid>", "new_theme", self.new_theme,
            methods=["GET"]
        )
        # create
        app.add_url_rule(
            "/themes/create", "create_theme", self.create_theme,
            methods=["POST"]
        )
        app.add_url_rule(
            "/themes/create/<int:gid>", "create_theme", self.create_theme,
            methods=["POST"]
        )
        # edit
        app.add_url_rule(
            "/themes/edit/<int:tid>", "edit_theme", self.edit_theme,
            methods=["GET"]
        )
        app.add_url_rule(
            "/themes/edit/<int:tid>/<int:gid>", "edit_theme", self.edit_theme,
            methods=["GET"]
        )
        # update
        app.add_url_rule(
            "/themes/update/<int:tid>", "update_theme",
            self.update_theme, methods=["POST"]
        )
        app.add_url_rule(
            "/themes/update/<int:tid>/<int:gid>", "update_theme",
            self.update_theme, methods=["POST"]
        )
        # delete
        app.add_url_rule(
            "/themes/delete/<int:tid>", "delete_theme",
            self.delete_theme, methods=["GET"]
        )
        app.add_url_rule(
            "/themes/delete/<int:tid>/<int:gid>", "delete_theme",
            self.delete_theme, methods=["GET"]
        )
        # move
        app.add_url_rule(
            "/themes/move/<string:direction>/<int:tid>",
            "move_theme", self.move_theme, methods=["GET"]
        )
        app.add_url_rule(
            "/themes/move/<string:direction>/<int:tid>/<int:gid>",
            "move_theme", self.move_theme, methods=["GET"]
        )

        # add group
        app.add_url_rule(
            "/themes/add_theme_group", "add_theme_group", self.add_theme_group,
            methods=["GET"]
        )
        # delete group
        app.add_url_rule(
            "/themes/delete_theme_group/<int:gid>", "delete_theme_group",
            self.delete_theme_group, methods=["GET"]
        )
        # update group
        app.add_url_rule(
            "/themes/update_theme_group/<int:gid>", "update_theme_group",
            self.update_theme_group, methods=["POST"]
        )
        # move group
        app.add_url_rule(
            "/themes/move_theme_group/<string:direction>/<int:gid>",
            "move_theme_group", self.move_theme_group, methods=["GET"]
        )

        # save themesconfig
        app.add_url_rule(
            "/themes/save_themesconfig", "save_themesconfig",
            self.save_themesconfig, methods=["GET"]
        )
        # reset themesconfig
        app.add_url_rule(
            "/themes/reset_themesconfig", "reset_themesconfig",
            self.reset_themesconfig, methods=["GET"]
        )

        self.app = app
        self.handler = handler
        self.themesconfig = themesconfig
        self.template_dir = "plugins/themes/templates"

        config_handler = handler()
        db_engine = config_handler.db_engine()
        self.config_models = ConfigModels(db_engine, config_handler.conn_str())
        self.resources = self.config_models.model('resources')

    def index(self):
        """Show theme list."""
        self.themesconfig = ThemeUtils.load_themesconfig(self.app, self.handler)
        themes = OrderedDict()
        themes["items"] = []
        themes["groups"] = []

        for item in self.themesconfig["themes"].get("items", []):
            themes["items"].append({
                "name": item["title"] if "title" in item else item["url"],
                "url": item["url"]
            })

        # TODO: nested groups
        for group in self.themesconfig["themes"].get("groups", []):
            groupEntry = {
                "title": group["title"],
                "items": []
            }
            for item in group["items"]:
                groupEntry["items"].append({
                    "name": item["title"] if "title" in item else item["url"]
                })
            themes["groups"].append(groupEntry)

        return render_template(
            "%s/themes.html" % self.template_dir, themes=themes,
            endpoint_suffix="theme", title="Theme configuration"
        )

    def new_theme(self, gid=None):
        """Show new theme form."""
        form = self.create_form()
        template = "%s/theme.html" % self.template_dir
        title = "Create theme"
        action = url_for("create_theme", gid=gid)

        return render_template(
            template, title=title, form=form, action=action, gid=gid,
            method="POST"
        )

    def create_theme(self, gid=None):
        """Create new theme."""
        form = self.create_form()
        if form.validate_on_submit():
            try:
                self.create_or_update_theme(None, form, gid=gid)
                flash("Theme {0} created.".format(form.title.data),
                      "success")
                return redirect(url_for("themes"))
            except ValidationError:
                flash("Could not create theme {0}.".format(
                    form.title.data), "warning")
        else:
            flash("Could not create theme {0}.".format(form.title.data),
                  "warning")

        # show validation errors
        template = "%s/theme.html" % self.template_dir
        title = "Theme configuration"
        action = url_for("create_theme", gid=gid)

        return render_template(
            template, title=title, form=form, action=action, gid=gid,
            method="POST"
        )

    def edit_theme(self, tid, gid=None):
        """Show edit theme form.

        :param int id: Theme ID
        """
        # find theme
        theme = self.find_theme(tid, gid)

        if theme is not None:
            template = "%s/theme.html" % self.template_dir
            form = self.create_form(theme)
            title = "Edit theme"
            action = url_for("update_theme", tid=tid, gid=gid)

            return render_template(
                template, title=title, form=form, action=action, theme=theme,
                tid=tid, gid=gid, method="POST"
            )
        else:
            # theme not found
            abort(404)

    def update_theme(self, tid, gid=None):
        """Update existing theme.

        :param int id: Theme ID
        """
        # find theme
        theme = self.find_theme(tid, gid)

        if theme is not None:
            form = self.create_form()

            if form.validate_on_submit():
                try:
                    # update theme
                    self.create_or_update_theme(theme, form, tid=tid, gid=gid)
                    flash("Theme {0} was updated.".format(
                        form.title.data), "success")
                    return redirect(url_for("themes"))
                except ValidationError:
                    flash("Could not update theme {0}.".format(
                        form.title.data), "warning")
            else:
                flash("Could not update theme {0}.".format(
                      form.title.data), "warning")

            # show validation errors
            template = "%s/theme.html" % self.template_dir
            title = "Update theme"
            action = url_for("update_theme", tid=tid, gid=gid)

            return render_template(
                template, title=title, form=form, action=action, tid=tid,
                gid=gid, method="POST"
            )

        else:
            # theme not found
            abort(404)

    def delete_theme(self, tid, gid=None):
        if gid is None:
            name = self.themesconfig["themes"]["items"][tid]["url"]
            name = name.split("/")[-1]
            self.themesconfig["themes"]["items"].pop(tid)
        else:
            name = self.themesconfig["themes"]["groups"][gid]["items"][tid]["url"]
            name = name.split("/")[-1]
            self.themesconfig["themes"]["groups"][gid]["items"].pop(tid)

        session = self.config_models.session()
        resource = session.query(self.resources).filter_by(
            type="map", name=name
        ).first()

        if resource:
            try:
                session.delete(resource)
                session.commit()
            except InternalError as e:
                flash("InternalError: %s" % e.orig, "error")
            except IntegrityError as e:
                flash("Could not delete resource for map '{0}'!".format(
                    resource.name), "warning")

        self.save_themesconfig()
        return redirect(url_for("themes"))

    def move_theme(self, direction, tid, gid=None):
        if gid is None:
            items = self.themesconfig["themes"]["items"]

            if direction == "up" and tid > 0:
                items[tid-1], items[tid] = items[tid], items[tid-1]

            elif direction == "down" and len(items)-1 > tid:
                items[tid], items[tid-1] = items[tid-1], items[tid]

            self.themesconfig["themes"]["items"] = items

        else:
            items = self.themesconfig["themes"]["groups"][gid]["items"]

            if direction == "up" and tid > 0:
                items[tid-1], items[tid] = items[tid], items[tid-1]

            elif direction == "down" and len(items)-1 > tid:
                items[tid], items[tid-1] = items[tid-1], items[tid]

            self.themesconfig["themes"]["groups"][gid]["items"] = items

        self.save_themesconfig()
        return redirect(url_for("themes"))

    def add_theme_group(self):
        self.themesconfig["themes"]["groups"] = self.themesconfig["themes"].get("groups", [])
        self.themesconfig["themes"]["groups"].append({
            "title": "new theme group",
            "items": []
        })
        self.save_themesconfig()
        return redirect(url_for("themes"))

    def delete_theme_group(self, gid):
        self.themesconfig["themes"]["groups"].pop(gid)
        self.save_themesconfig()
        return redirect(url_for("themes"))

    def update_theme_group(self, gid):
        self.themesconfig["themes"]["groups"][gid]["title"] = request.form[
            "group_title"]
        self.save_themesconfig()
        return redirect(url_for("themes"))

    def move_theme_group(self, gid, direction):
        groups = self.themesconfig["themes"]["groups"]

        if direction == "up" and gid > 1:
            groups[gid-1], groups[gid] = groups[gid], groups[gid-1]

        elif direction == "down" and len(groups) > gid:
            groups[gid], groups[gid-1] = groups[gid-1], groups[gid]

        self.themesconfig["themes"]["groups"] = groups
        self.save_themesconfig()
        return redirect(url_for("themes"))

    def save_themesconfig(self):
        if ThemeUtils.save_themesconfig(self.themesconfig, self.app, self.handler):
            flash("Theme configuration was saved.", "success")
        else:
            flash("Could not save theme configuration.",
                  "error")

        return redirect(url_for("themes"))

    def reset_themesconfig(self):
        self.themesconfig = ThemeUtils.load_themesconfig(self.app, self.handler)
        flash("Theme configuration reloaded from disk.", "warning")
        return redirect(url_for("themes"))

    def find_theme(self, tid, gid=None):
        """Find theme by ID.

        :param int id: Theme ID
        """
        if gid is None:
            for i, item in enumerate(self.themesconfig["themes"]["items"]):
                if i == tid:
                    return item
        else:
            for i, group in enumerate(self.themesconfig["themes"]["groups"]):
                if i == gid:
                    for j, item in enumerate(group["items"]):
                        if j == tid:
                            return item

        return None

    def create_form(self, theme=None):
        """Return form with fields loaded from themesConfig.json.

        :param object theme: Optional theme object
        """
        form = ThemeForm()
        if theme:
            form = ThemeForm(url=theme["url"])

        crslist = ThemeUtils.get_crs(self.app, self.handler)

        form.url.choices = [("", "---")] + ThemeUtils.get_projects(self.app, self.handler)
        form.thumbnail.choices = ThemeUtils.get_mapthumbs(self.app, self.handler)
        form.format.choices = ThemeUtils.get_format()
        form.mapCrs.choices = crslist
        form.additionalMouseCrs.choices = crslist
        form.backgroundLayersList = self.get_backgroundlayers()

        if form.backgroundLayers.data:
            for i in range(len(form.backgroundLayers.data)):
                form.backgroundLayers[i].layerName.choices = self.get_backgroundlayers()

        if theme is None:
            return form
        else:
            current_handler = self.handler()
            ogc_service_url = current_handler.config().get("ogc_service_url")
            if "url" in theme:
                if theme["url"].startswith(ogc_service_url):
                    form.url.data = theme["url"]
                else:
                    form.url.data = ogc_service_url.rstrip("/") + "/" + theme["url"]
            else:
                form.url.data = None
            if "title" in theme:
                form.title.data = theme["title"]
            if "default" in theme:
                form.default.data = theme["default"]
            if "thumbnail" in theme:
                form.thumbnail.data = theme["thumbnail"]
            if "attribution" in theme:
                form.attribution.data = theme["attribution"]
            # TODO: FORM attributionUrl
            # if "attributionUrl" in theme:
            #    form.attribution.data = theme["attributionUrl"]
            if "format" in theme:
                form.format.data = theme["format"]
            if "mapCrs" in theme:
                form.mapCrs.data = theme["mapCrs"]
            if "additionalMouseCrs" in theme:
                form.additionalMouseCrs.data = theme["additionalMouseCrs"]
            if "searchProviders" in theme:
                searchProviders = []
                for provider in theme["searchProviders"]:
                    if "key" in provider:
                        searchProviders.append(provider["key"])
                    else:
                        searchProviders.append(provider)
                form.searchProviders.data = ",".join(searchProviders)
            if "scales" in theme:
                form.scales.data = ", ".join(map(str, theme["scales"]))
            if "printScales" in theme:
                form.printScales.data = ", ".join(map(str, theme[
                    "printScales"]))
            if "printResolutions" in theme:
                form.printResolutions.data = ", ".join(map(str, theme[
                    "printResolutions"]))
            if "printLabelBlacklist" in theme:
                form.printLabelBlacklist.data = ", ".join(map(str, theme[
                    "printLabelBlacklist"]))
            if "skipEmptyFeatureAttributes" in theme:
                form.skipEmptyFeatureAttributes.data = theme["skipEmptyFeatureAttributes"]
            if "collapseLayerGroupsBelowLevel" in theme:
                form.collapseLayerGroupsBelowLevel.data = theme["collapseLayerGroupsBelowLevel"]

            if "backgroundLayers" in theme:
                for i, layer in enumerate(theme["backgroundLayers"]):
                    data = {
                        "layerName": ("", ""),
                        "printLayer": "",
                        "visibility": False
                    }

                    for l in self.get_backgroundlayers():
                        if layer["name"] == l[0]:
                            data["layerName"] = l

                    if "printLayer" in layer:
                        data["printLayer"] = layer["printLayer"]

                    if "visibility" in layer:
                        data["visibility"] = layer["visibility"]

                    form.backgroundLayers.append_entry(data)
                    form.backgroundLayers[i].layerName.choices = self.get_backgroundlayers()
                    form.backgroundLayers[i].layerName.data = layer["name"]

            return form

    def create_or_update_theme(self, theme, form, tid=None, gid=None):
        """Create or update theme records in Themesconfig.

        :param object theme: Optional theme object
                                (None for create)
        :param FlaskForm form: Form for theme
        """
        item = OrderedDict()
        item["url"] = form.url.data

        if form.title.data:
            item["title"] = form.title.data
        else:
            if "title" in item: del item["title"]

        item["default"] = False
        if form.default.data:
            item["default"] = True

        if form.thumbnail.data:
            item["thumbnail"] = form.thumbnail.data

        item["attribution"] = ""
        if form.attribution.data:
            item["attribution"] = form.attribution.data

        # TODO: FORM attributionUrl
        item["attributionUrl"] = ""

        if form.format.data:
            item["format"] = form.format.data
        else:
            if "format" in item: del item["format"]

        if form.mapCrs.data:
            item["mapCrs"] = form.mapCrs.data
        else:
            if item in "mapCrs": del item["mapCrs"]

        if form.additionalMouseCrs.data:
            item["additionalMouseCrs"] = form.additionalMouseCrs.data
        else:
            if "additionalMouseCrs" in item: del item["additionalMouseCrs"]

        item["searchProviders"] = []
        if form.searchProviders.data:
            for provider in form.searchProviders.data.split(","):
                item["searchProviders"].append(provider)

        if form.scales.data:
            item["scales"] = list(map(int, form.scales.data.replace(
                " ", "").split(",")))
        else:
            if "scales" in item: del item["scales"]

        if form.printScales.data:
            item["printScales"] = list(map(int, form.printScales.data.replace(
                " ", "").split(",")))
        else:
            if "printScales" in item: del item["printScales"]

        if form.printResolutions.data:
            item["printResolutions"] = list(map(
                int, form.printResolutions.data.replace(" ", "").split(",")))
        else:
            if "printResolutions" in item: del item["printResolutions"]

        if form.printLabelBlacklist.data:
            item["printLabelBlacklist"] = list(map(
                str, form.printLabelBlacklist.data.replace(" ", "").split(",")
            ))
        else:
            if "printLabelBlacklist" in item: del item["printLabelBlacklist"]

        item["skipEmptyFeatureAttributes"] = False
        if form.skipEmptyFeatureAttributes.data:
            item["skipEmptyFeatureAttributes"] = True

        if form.collapseLayerGroupsBelowLevel.data:
            item["collapseLayerGroupsBelowLevel"] = form.collapseLayerGroupsBelowLevel.data
        else:
            if "collapseLayerGroupsBelowLevel" in item: del item["collapseLayerGroupsBelowLevel"]

        item["backgroundLayers"] = []
        if form.backgroundLayers.data:
            for layer in form.backgroundLayers.data:
                item["backgroundLayers"].append({
                    "name": layer["layerName"],
                    "printLayer": layer["printLayer"],
                    "visibility": layer["visibility"]
                })

        new_name = form.url.data.split("/")[-1]
        session = self.config_models.session()

        # edit theme
        if theme:
            if gid is None:
                name = self.themesconfig["themes"]["items"][tid]["url"]
                self.themesconfig["themes"]["items"][tid] = item
            else:
                name = self.themesconfig["themes"]["groups"][gid]["items"][tid]["url"]
                self.themesconfig["themes"]["groups"][gid]["items"][tid] = item

            name = name.split("/")[-1]
            resource = session.query(self.resources).filter_by(name=name).first()
            if resource:
                resource.name = new_name
                try:
                    session.commit()
                except InternalError as e:
                    flash("InternalError: {0}".format(e.orig), "error")
                except IntegrityError as e:
                    flash("Resource for map '{0}' could not be edited!".format(
                        resource.name), "warning")

        # new theme
        else:
            resource = self.resources()
            resource.type = "map"
            resource.name = new_name
            try:
                session.add(resource)
                session.commit()
            except InternalError as e:
                flash("InternalError: {0}".format(e.orig), "error")
            except IntegrityError as e:
                flash("Resource for map '{0}' already exists!".format(
                    resource.name), "warning")

            if gid is None:
                self.themesconfig["themes"]["items"].append(item)
            else:
                self.themesconfig["themes"]["groups"][gid]["items"].append(
                    item)

        self.save_themesconfig()

    def get_backgroundlayers(self):
        layers = []
        for layer in self.themesconfig["themes"]["backgroundLayers"]:
            layers.append((layer["name"], layer["name"]))
        return layers