def on_init(self): # Retain a pointer to this object for use in routes global ui_instance ui_instance = self # Main references to basic components (if initialized) self.http_server = None self.socket_io = None self.service_gateway = None self.oauth = oauth # Configuration self.server_enabled = self.CFG.get_safe(CFG_PREFIX + ".server.enabled") is True self.server_debug = self.CFG.get_safe(CFG_PREFIX + ".server.debug") is True # Note: this may be the empty string. Using localhost does not make the server publicly accessible self.server_hostname = self.CFG.get_safe(CFG_PREFIX + ".server.hostname", DEFAULT_WEB_SERVER_HOSTNAME) self.server_port = self.CFG.get_safe(CFG_PREFIX + ".server.port", DEFAULT_WEB_SERVER_PORT) self.server_log_access = self.CFG.get_safe(CFG_PREFIX + ".server.log_access") is True self.server_log_errors = self.CFG.get_safe(CFG_PREFIX + ".server.log_errors") is True self.server_socket_io = self.CFG.get_safe(CFG_PREFIX + ".server.socket_io") is True self.server_secret = self.CFG.get_safe(CFG_PREFIX + ".security.secret") or "" self.session_timeout = int(self.CFG.get_safe(CFG_PREFIX + ".security.session_timeout") or DEFAULT_SESSION_TIMEOUT) self.extend_session_timeout = self.CFG.get_safe(CFG_PREFIX + ".security.extend_session_timeout") is True self.max_session_validity = int(self.CFG.get_safe(CFG_PREFIX + ".security.max_session_validity") or DEFAULT_SESSION_TIMEOUT) self.remember_user = self.CFG.get_safe(CFG_PREFIX + ".security.remember_user") is True self.set_cors_headers = self.CFG.get_safe(CFG_PREFIX + ".server.set_cors") is True self.develop_mode = self.CFG.get_safe(CFG_PREFIX + ".server.develop_mode") is True self.oauth_enabled = self.CFG.get_safe(CFG_PREFIX + ".oauth.enabled") is True self.oauth_scope = self.CFG.get_safe(CFG_PREFIX + ".oauth.default_scope") or "scioncc" self.has_service_gateway = self.CFG.get_safe(CFG_PREFIX + ".service_gateway.enabled") is True self.service_gateway_prefix = self.CFG.get_safe(CFG_PREFIX + ".service_gateway.url_prefix", DEFAULT_GATEWAY_PREFIX) self.extensions = self.CFG.get_safe(CFG_PREFIX + ".extensions") or [] self.extension_objs = [] # TODO: What about https? self.base_url = "http://%s:%s" % (self.server_hostname or "localhost", self.server_port) self.gateway_base_url = None self.idm_client = IdentityManagementServiceProcessClient(process=self) # One time setup if self.server_enabled: app.secret_key = self.server_secret or self.__class__.__name__ # Enables encrypted session cookies if self.server_debug: app.debug = True if self.server_socket_io: self.socket_io = SocketIO(app) if self.has_service_gateway: from ion.service.service_gateway import ServiceGateway, sg_blueprint self.gateway_base_url = self.base_url + self.service_gateway_prefix self.service_gateway = ServiceGateway(process=self, config=self.CFG, response_class=app.response_class) app.register_blueprint(sg_blueprint, url_prefix=self.service_gateway_prefix) for ext_cls in self.extensions: try: cls = named_any(ext_cls) except AttributeError as ae: # Try to nail down the error import importlib importlib.import_module(ext_cls.rsplit(".", 1)[0]) raise self.extension_objs.append(cls()) for ext_obj in self.extension_objs: ext_obj.on_init(self, app) if self.extensions: log.info("UI Server: %s extensions initialized", len(self.extensions)) # Start the web server self.start_service() log.info("UI Server: Started server on %s" % self.base_url) else: log.warn("UI Server: Server disabled in config")
class UIServer(SimpleProcess): """ Process to start a generic UI server that can be extended with content and service gateway """ def on_init(self): # Retain a pointer to this object for use in routes global ui_instance ui_instance = self # Main references to basic components (if initialized) self.http_server = None self.socket_io = None self.service_gateway = None self.oauth = oauth # Configuration self.server_enabled = self.CFG.get_safe(CFG_PREFIX + ".server.enabled") is True self.server_debug = self.CFG.get_safe(CFG_PREFIX + ".server.debug") is True # Note: this may be the empty string. Using localhost does not make the server publicly accessible self.server_hostname = self.CFG.get_safe(CFG_PREFIX + ".server.hostname", DEFAULT_WEB_SERVER_HOSTNAME) self.server_port = self.CFG.get_safe(CFG_PREFIX + ".server.port", DEFAULT_WEB_SERVER_PORT) self.server_log_access = self.CFG.get_safe(CFG_PREFIX + ".server.log_access") is True self.server_log_errors = self.CFG.get_safe(CFG_PREFIX + ".server.log_errors") is True self.server_socket_io = self.CFG.get_safe(CFG_PREFIX + ".server.socket_io") is True self.server_secret = self.CFG.get_safe(CFG_PREFIX + ".security.secret") or "" self.session_timeout = int(self.CFG.get_safe(CFG_PREFIX + ".security.session_timeout") or DEFAULT_SESSION_TIMEOUT) self.extend_session_timeout = self.CFG.get_safe(CFG_PREFIX + ".security.extend_session_timeout") is True self.max_session_validity = int(self.CFG.get_safe(CFG_PREFIX + ".security.max_session_validity") or DEFAULT_SESSION_TIMEOUT) self.remember_user = self.CFG.get_safe(CFG_PREFIX + ".security.remember_user") is True self.set_cors_headers = self.CFG.get_safe(CFG_PREFIX + ".server.set_cors") is True self.develop_mode = self.CFG.get_safe(CFG_PREFIX + ".server.develop_mode") is True self.oauth_enabled = self.CFG.get_safe(CFG_PREFIX + ".oauth.enabled") is True self.oauth_scope = self.CFG.get_safe(CFG_PREFIX + ".oauth.default_scope") or "scioncc" self.has_service_gateway = self.CFG.get_safe(CFG_PREFIX + ".service_gateway.enabled") is True self.service_gateway_prefix = self.CFG.get_safe(CFG_PREFIX + ".service_gateway.url_prefix", DEFAULT_GATEWAY_PREFIX) self.extensions = self.CFG.get_safe(CFG_PREFIX + ".extensions") or [] self.extension_objs = [] # TODO: What about https? self.base_url = "http://%s:%s" % (self.server_hostname or "localhost", self.server_port) self.gateway_base_url = None self.idm_client = IdentityManagementServiceProcessClient(process=self) # One time setup if self.server_enabled: app.secret_key = self.server_secret or self.__class__.__name__ # Enables encrypted session cookies if self.server_debug: app.debug = True if self.server_socket_io: self.socket_io = SocketIO(app) if self.has_service_gateway: from ion.service.service_gateway import ServiceGateway, sg_blueprint self.gateway_base_url = self.base_url + self.service_gateway_prefix self.service_gateway = ServiceGateway(process=self, config=self.CFG, response_class=app.response_class) app.register_blueprint(sg_blueprint, url_prefix=self.service_gateway_prefix) for ext_cls in self.extensions: try: cls = named_any(ext_cls) except AttributeError as ae: # Try to nail down the error import importlib importlib.import_module(ext_cls.rsplit(".", 1)[0]) raise self.extension_objs.append(cls()) for ext_obj in self.extension_objs: ext_obj.on_init(self, app) if self.extensions: log.info("UI Server: %s extensions initialized", len(self.extensions)) # Start the web server self.start_service() log.info("UI Server: Started server on %s" % self.base_url) else: log.warn("UI Server: Server disabled in config") def on_quit(self): self.stop_service() def start_service(self): """ Starts the web server. """ if self.http_server is not None: self.stop_service() if self.server_socket_io: self.http_server = SocketIOServer((self.server_hostname, self.server_port), app.wsgi_app, resource='socket.io', log=None) self.http_server._gl = gevent.spawn(self.http_server.serve_forever) log.info("UI Server: Providing web sockets (socket.io) server") else: self.http_server = WSGIServer((self.server_hostname, self.server_port), app, log=None) self.http_server.start() if self.service_gateway: self.service_gateway.start() log.info("UI Server: Service Gateway started on %s", self.gateway_base_url) for ext_obj in self.extension_objs: ext_obj.on_start() return True def stop_service(self): """ Responsible for stopping the gevent based web server. """ for ext_obj in self.extension_objs: ext_obj.on_stop() if self.http_server is not None: self.http_server.stop() if self.service_gateway: self.service_gateway.stop() # Need to terminate the server greenlet? return True # ------------------------------------------------------------------------- # Authentication def auth_external(self, username, ext_user_id, ext_id_provider="ext"): """ Given username and user identifier from an external identity provider (IdP), retrieve actor_id and establish user session. Return user info from session. Convention is that system local username is ext_id_provider + ":" + username, e.g. "ext_johnbean" Return NotFound if user not registered in system. Caller can react and create a user account through the normal system means @param username the user name the user recognizes. @param ext_user_id a unique identifier coming from the external IdP @param ext_id_provider identifies the external IdP service """ try: if ext_user_id and ext_id_provider and username: local_username = "******" % (ext_id_provider, username) actor_id = self.idm_client.find_actor_identity_by_username(local_username) user_info = self._set_server_session(actor_id, local_username) return build_json_response(user_info) else: raise BadRequest("External user info missing") except Exception: return build_json_error() def login(self): """ Explicit (non-token) login and creation of a server session (Cookie based). """ try: username = get_arg("username") password = get_arg("password") if username and password: actor_id = self.idm_client.check_actor_credentials(username, password) user_info = self._set_server_session(actor_id, username) return build_json_response(user_info) else: raise BadRequest("Username or password missing") except Exception: return build_json_error() def _set_server_session(self, actor_id, username=None): """ Sets server session based on user_id and ActorIdentity. """ actor_user = self.idm_client.read_identity_details(actor_id) if actor_user.type_ != OT.UserIdentityDetails: raise BadRequest("Bad identity details") full_name = actor_user.contact.individual_names_given + " " + actor_user.contact.individual_name_family valid_until = int(get_ion_ts_millis() / 1000 + self.session_timeout) set_auth(actor_id, username, full_name, valid_until=valid_until, roles=actor_user.contact.roles) user_info = get_auth() return user_info def get_session(self): """ Returns user session information for current authentication. This can be polled regularly by client code to detect changes in session state and expiration. """ def call_extend_session_attrs(session_attrs, actor_user): """ Call UI extensions to make additions to user session """ for ext_obj in self.extension_objs: func = getattr(ext_obj, "extend_user_session_attributes", None) if func: try: func(session_attrs, actor_user) except Exception: log.exception("Error calling UI extension extend_user_session_attributes()") try: # Get user session from OAuth access token in HTTP Authorization header auth_hdr = request.headers.get("authorization", None) if auth_hdr: valid, req = self.oauth.verify_request([self.oauth_scope]) # Note: Do NOT extend session timeout here! if valid: actor_id = flask.g.oauth_user.get("actor_id", "") actor_user = self.idm_client.read_actor_identity(actor_id) session_attrs = dict(is_logged_in=True, is_registered=True, attributes={"roles": actor_user.details.contact.roles}, roles={}) if actor_user.session: session_attrs.update(actor_user.session) call_extend_session_attrs(session_attrs, actor_user) return build_json_response(session_attrs) if self.remember_user: # Get user session from user_id/access_token placed inside server session (Cookie) # This is a feature to allow returning users to resume a session if still valid access_token = flask.session.get("access_token", None) actor_id = flask.session.get("actor_id", None) if access_token and actor_id: actor_user = self.idm_client.read_actor_identity(actor_id) session_attrs = dict(access_token=access_token, is_logged_in=True, is_registered=True, attributes={"roles": actor_user.details.contact.roles}, roles={}) if actor_user.session: # Check validity in persisted user session if 0 < int(actor_user.session.get("valid_until", 0)) * 1000 < current_time_millis(): clear_auth() return build_json_response(get_auth()) session_attrs.update(actor_user.session) else: # No trace of existing session in user object clear_auth() return build_json_response(get_auth()) call_extend_session_attrs(session_attrs, actor_user) return build_json_response(session_attrs) # Get user session from Flask session and cookie (non-token mode) user_info = get_auth() if 0 < int(user_info.get("valid_until", 0)) * 1000 < current_time_millis(): clear_auth() # Clear expired session user_info = get_auth() call_extend_session_attrs(user_info, None) return build_json_response(user_info) except Exception: return build_json_error() def logout(self): try: access_token = get_req_bearer_token() or flask.session.get("access_token", None) if access_token: try: # Invalidate access token token_id = str("access_token_%s" % access_token) token_obj = ui_instance.container.object_store.read(token_id) token_obj.status = "CANCELLED" token_obj.attributes["cancel_ts"] = get_ion_ts_millis() token_obj.attributes["cancel_msg"] = "User logout" ui_instance.container.object_store.update(token_obj) log.info("Invalidated stored access token for user=%s", token_obj.actor_id) except NotFound: pass except Exception: log.exception("Error invalidating access token") clear_auth() return build_json_response("OK") except Exception: return build_json_error()