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')
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 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 __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)
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
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)
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)
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
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)
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
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
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')
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