def _download_image(self) -> Tuple[str, str]: """Downloads an image into the URL cache. Returns: A (host, media_id) tuple representing the MXC URI of the image. """ self.lookups["cdn.twitter.com"] = [(IPv4Address, "10.1.2.3")] channel = self.make_request( "GET", "preview_url?url=http://cdn.twitter.com/matrixdotorg", shorthand=False, await_result=False, ) self.pump() client = self.reactor.tcpClients[0][2].buildProtocol(None) server = AccumulatingProtocol() server.makeConnection(FakeTransport(client, self.reactor)) client.makeConnection(FakeTransport(server, self.reactor)) client.dataReceived( b"HTTP/1.0 200 OK\r\nContent-Length: %d\r\nContent-Type: image/png\r\n\r\n" % (len(SMALL_PNG), ) + SMALL_PNG) self.pump() self.assertEqual(channel.code, 200) body = channel.json_body mxc_uri = body["og:image"] host, _port, media_id = parse_and_validate_mxc_uri(mxc_uri) self.assertIsNone(_port) return host, media_id
async def check_avatar_size_and_mime_type(self, mxc: str) -> bool: """Check that the size and content type of the avatar at the given MXC URI are within the configured limits. Args: mxc: The MXC URI at which the avatar can be found. Returns: A boolean indicating whether the file can be allowed to be set as an avatar. """ if not self.max_avatar_size and not self.allowed_avatar_mimetypes: return True server_name, _, media_id = parse_and_validate_mxc_uri(mxc) if server_name == self.server_name: media_info = await self.store.get_local_media(media_id) else: media_info = await self.store.get_cached_remote_media( server_name, media_id) if media_info is None: # Both configuration options need to access the file's metadata, and # retrieving remote avatars just for this becomes a bit of a faff, especially # if e.g. the file is too big. It's also generally safe to assume most files # used as avatar are uploaded locally, or if the upload didn't happen as part # of a PUT request on /avatar_url that the file was at least previewed by the # user locally (and therefore downloaded to the remote media cache). logger.warning( "Forbidding avatar change to %s: avatar not on server", mxc) return False if self.max_avatar_size: # Ensure avatar does not exceed max allowed avatar size if media_info["media_length"] > self.max_avatar_size: logger.warning( "Forbidding avatar change to %s: %d bytes is above the allowed size " "limit", mxc, media_info["media_length"], ) return False if self.allowed_avatar_mimetypes: # Ensure the avatar's file type is allowed if (self.allowed_avatar_mimetypes and media_info["media_type"] not in self.allowed_avatar_mimetypes): logger.warning( "Forbidding avatar change to %s: mimetype %s not allowed", mxc, media_info["media_type"], ) return False return True
def _parse_oidc_config_dict( oidc_config: JsonDict, config_path: Tuple[str, ...]) -> "OidcProviderConfig": """Take the configuration dict and parse it into an OidcProviderConfig Raises: ConfigError if the configuration is malformed. """ ump_config = oidc_config.get("user_mapping_provider", {}) ump_config.setdefault("module", DEFAULT_USER_MAPPING_PROVIDER) if ump_config.get("module") == LEGACY_USER_MAPPING_PROVIDER: ump_config["module"] = DEFAULT_USER_MAPPING_PROVIDER ump_config.setdefault("config", {}) ( user_mapping_provider_class, user_mapping_provider_config, ) = load_module(ump_config, config_path + ("user_mapping_provider", )) # Ensure loaded user mapping module has defined all necessary methods required_methods = [ "get_remote_user_id", "map_user_attributes", ] missing_methods = [ method for method in required_methods if not hasattr(user_mapping_provider_class, method) ] if missing_methods: raise ConfigError( "Class %s is missing required " "methods: %s" % ( user_mapping_provider_class, ", ".join(missing_methods), ), config_path + ("user_mapping_provider", "module"), ) idp_id = oidc_config.get("idp_id", "oidc") # prefix the given IDP with a prefix specific to the SSO mechanism, to avoid # clashes with other mechs (such as SAML, CAS). # # We allow "oidc" as an exception so that people migrating from old-style # "oidc_config" format (which has long used "oidc" as its idp_id) can migrate to # a new-style "oidc_providers" entry without changing the idp_id for their provider # (and thereby invalidating their user_external_ids data). if idp_id != "oidc": idp_id = "oidc-" + idp_id # MSC2858 also specifies that the idp_icon must be a valid MXC uri idp_icon = oidc_config.get("idp_icon") if idp_icon is not None: try: parse_and_validate_mxc_uri(idp_icon) except ValueError as e: raise ConfigError("idp_icon must be a valid MXC URI", config_path + ("idp_icon", )) from e client_secret_jwt_key_config = oidc_config.get("client_secret_jwt_key") client_secret_jwt_key: Optional[OidcProviderClientSecretJwtKey] = None if client_secret_jwt_key_config is not None: keyfile = client_secret_jwt_key_config.get("key_file") if keyfile: key = read_file(keyfile, config_path + ("client_secret_jwt_key", )) else: key = client_secret_jwt_key_config["key"] client_secret_jwt_key = OidcProviderClientSecretJwtKey( key=key, jwt_header=client_secret_jwt_key_config["jwt_header"], jwt_payload=client_secret_jwt_key_config.get("jwt_payload", {}), ) # parse attribute_requirements from config (list of dicts) into a list of SsoAttributeRequirement attribute_requirements = [ SsoAttributeRequirement(**x) for x in oidc_config.get("attribute_requirements", []) ] return OidcProviderConfig( idp_id=idp_id, idp_name=oidc_config.get("idp_name", "OIDC"), idp_icon=idp_icon, idp_brand=oidc_config.get("idp_brand"), discover=oidc_config.get("discover", True), issuer=oidc_config["issuer"], client_id=oidc_config["client_id"], client_secret=oidc_config.get("client_secret"), client_secret_jwt_key=client_secret_jwt_key, client_auth_method=oidc_config.get("client_auth_method", "client_secret_basic"), scopes=oidc_config.get("scopes", ["openid"]), authorization_endpoint=oidc_config.get("authorization_endpoint"), token_endpoint=oidc_config.get("token_endpoint"), userinfo_endpoint=oidc_config.get("userinfo_endpoint"), jwks_uri=oidc_config.get("jwks_uri"), skip_verification=oidc_config.get("skip_verification", False), user_profile_method=oidc_config.get("user_profile_method", "auto"), allow_existing_users=oidc_config.get("allow_existing_users", False), user_mapping_provider_class=user_mapping_provider_class, user_mapping_provider_config=user_mapping_provider_config, attribute_requirements=attribute_requirements, )
def _parse_oidc_config_dict( oidc_config: JsonDict, config_path: Tuple[str, ...]) -> "OidcProviderConfig": """Take the configuration dict and parse it into an OidcProviderConfig Raises: ConfigError if the configuration is malformed. """ ump_config = oidc_config.get("user_mapping_provider", {}) ump_config.setdefault("module", DEFAULT_USER_MAPPING_PROVIDER) ump_config.setdefault("config", {}) ( user_mapping_provider_class, user_mapping_provider_config, ) = load_module(ump_config, config_path + ("user_mapping_provider", )) # Ensure loaded user mapping module has defined all necessary methods required_methods = [ "get_remote_user_id", "map_user_attributes", ] missing_methods = [ method for method in required_methods if not hasattr(user_mapping_provider_class, method) ] if missing_methods: raise ConfigError( "Class %s is missing required " "methods: %s" % ( user_mapping_provider_class, ", ".join(missing_methods), ), config_path + ("user_mapping_provider", "module"), ) # MSC2858 will apply certain limits in what can be used as an IdP id, so let's # enforce those limits now. # TODO: factor out this stuff to a generic function idp_id = oidc_config.get("idp_id", "oidc") # TODO: update this validity check based on what MSC2858 decides. valid_idp_chars = set(string.ascii_lowercase + string.digits + "-._") if any(c not in valid_idp_chars for c in idp_id): raise ConfigError( 'idp_id may only contain a-z, 0-9, "-", ".", "_"', config_path + ("idp_id", ), ) if idp_id[0] not in string.ascii_lowercase: raise ConfigError( "idp_id must start with a-z", config_path + ("idp_id", ), ) # prefix the given IDP with a prefix specific to the SSO mechanism, to avoid # clashes with other mechs (such as SAML, CAS). # # We allow "oidc" as an exception so that people migrating from old-style # "oidc_config" format (which has long used "oidc" as its idp_id) can migrate to # a new-style "oidc_providers" entry without changing the idp_id for their provider # (and thereby invalidating their user_external_ids data). if idp_id != "oidc": idp_id = "oidc-" + idp_id # MSC2858 also specifies that the idp_icon must be a valid MXC uri idp_icon = oidc_config.get("idp_icon") if idp_icon is not None: try: parse_and_validate_mxc_uri(idp_icon) except ValueError as e: raise ConfigError("idp_icon must be a valid MXC URI", config_path + ("idp_icon", )) from e return OidcProviderConfig( idp_id=idp_id, idp_name=oidc_config.get("idp_name", "OIDC"), idp_icon=idp_icon, discover=oidc_config.get("discover", True), issuer=oidc_config["issuer"], client_id=oidc_config["client_id"], client_secret=oidc_config["client_secret"], client_auth_method=oidc_config.get("client_auth_method", "client_secret_basic"), scopes=oidc_config.get("scopes", ["openid"]), authorization_endpoint=oidc_config.get("authorization_endpoint"), token_endpoint=oidc_config.get("token_endpoint"), userinfo_endpoint=oidc_config.get("userinfo_endpoint"), jwks_uri=oidc_config.get("jwks_uri"), skip_verification=oidc_config.get("skip_verification", False), user_profile_method=oidc_config.get("user_profile_method", "auto"), allow_existing_users=oidc_config.get("allow_existing_users", False), user_mapping_provider_class=user_mapping_provider_class, user_mapping_provider_config=user_mapping_provider_config, )