Example #1
0
    def delete_dns_record(self, record):
        """
        Delete a record from the domain.
        """
        lexicon_config = self._get_base_config()
        lexicon_config['domain'] = record['domain']
        lexicon_config['action'] = 'delete'
        lexicon_config['name'] = record['name']
        lexicon_config['type'] = record['type']
        config = ConfigResolver()
        config.with_dict(dict_object=lexicon_config)
        client = Client(config)
        result = False
        try:
            result = client.execute()

            # Invalidate cache for the domain-cname pair
            cache.delete(f"{record['domain']}-{record['type']}")
        except Exception as e:  # pylint: disable=broad-except
            # This ugly checking of the exception message is needed
            # as the library only throws an instance of the Exception class.
            if 'Record identifier could not be found' in str(e):
                result = True
            else:
                raise
        return result
Example #2
0
def main() -> None:
    """Main function of Lexicon."""
    # Dynamically determine all the providers available and gather command line arguments.
    parsed_args = generate_cli_main_parser().parse_args()

    log_level = logging.getLevelName(parsed_args.log_level)
    logging.basicConfig(stream=sys.stdout, level=log_level, format="%(message)s")
    logger.debug("Arguments: %s", parsed_args)

    # In the CLI context, will get configuration interactively:
    #   * from the command line
    #   * from the environment variables
    #   * from lexicon configuration files found in given --config-dir (default is current dir)
    config = ConfigResolver()
    config.with_args(parsed_args).with_env().with_config_dir(parsed_args.config_dir)

    client = Client(config)

    results = client.execute()

    action = config.resolve("lexicon:action")
    if not action:
        raise ValueError("Parameter action is not set.")

    handle_output(results, parsed_args.output, action)
Example #3
0
def _txt_challenge(
    profile: Dict[str, Any],
    token: str,
    domain: str,
    action: str = "create",
):
    profile_name = profile["name"]
    provider_name = profile["provider"]
    provider_options = profile.get("provider_options", {})

    if not provider_options:
        print(f"No provider_options are defined for profile {profile_name}, "
              "any call to the provider API is likely to fail.")

    config_dict = {
        "action": action,
        "domain": domain,
        "type": "TXT",
        "name": "_acme-challenge.{0}.".format(domain),
        "content": token,
        "delegated": profile.get("delegated_subdomain"),
        "provider_name": provider_name,
        provider_name: provider_options,
    }

    ttl = profile.get("ttl")
    if ttl:
        config_dict["ttl"] = ttl

    lexicon_config = ConfigResolver()
    lexicon_config.with_dict(config_dict)

    Client(lexicon_config).execute()
Example #4
0
def test_dict_resolution():
    dict_object = {"delegated": "TEST1", "cloudflare": {"auth_token": "TEST2"}}

    config = ConfigResolver().with_dict(dict_object)

    assert config.resolve("lexicon:delegated") == "TEST1"
    assert config.resolve("lexicon:cloudflare:auth_token") == "TEST2"
    assert config.resolve("lexicon:nonexistent") is None
Example #5
0
def test_dict_resolution():
    dict_object = {'delegated': 'TEST1', 'cloudflare': {'auth_token': 'TEST2'}}

    config = ConfigResolver().with_dict(dict_object)

    assert config.resolve('lexicon:delegated') == 'TEST1'
    assert config.resolve('lexicon:cloudflare:auth_token') == 'TEST2'
    assert config.resolve('lexicon:nonexistent') is None
Example #6
0
def test_config_lexicon_file_resolution(tmpdir):
    lexicon_file = tmpdir.join("lexicon.yml")
    lexicon_file.write("delegated: TEST1\ncloudflare:\n  auth_token: TEST2")

    config = ConfigResolver().with_config_file(str(lexicon_file))

    assert config.resolve("lexicon:delegated") == "TEST1"
    assert config.resolve("lexicon:cloudflare:auth_token") == "TEST2"
    assert config.resolve("lexicon:nonexistent") is None
Example #7
0
def test_provider_config_lexicon_file_resolution(tmpdir):
    provider_file = tmpdir.join("lexicon_cloudflare.yml")
    provider_file.write("auth_token: TEST2")

    config = ConfigResolver().with_provider_config_file(
        "cloudflare", str(provider_file))

    assert config.resolve("lexicon:cloudflare:auth_token") == "TEST2"
    assert config.resolve("lexicon:nonexistent") is None
Example #8
0
def test_generic_config_feeder_resolution():
    class GenericConfigSource(ConfigSource):
        def resolve(self, config_key):
            return 'TEST1'

    config = ConfigResolver().with_config_source(GenericConfigSource())

    assert config.resolve('lexicon:cloudflare:auth_username') == 'TEST1'
    assert config.resolve('lexicon:nonexistent') == 'TEST1'
Example #9
0
def test_config_lexicon_file_resolution(tmpdir):
    lexicon_file = tmpdir.join('lexicon.yml')
    lexicon_file.write('delegated: TEST1\ncloudflare:\n  auth_token: TEST2')

    config = ConfigResolver().with_config_file(str(lexicon_file))

    assert config.resolve('lexicon:delegated') == 'TEST1'
    assert config.resolve('lexicon:cloudflare:auth_token') == 'TEST2'
    assert config.resolve('lexicon:nonexistent') is None
Example #10
0
def test_provider_config_lexicon_file_resolution(tmpdir):
    provider_file = tmpdir.join('lexicon_cloudflare.yml')
    provider_file.write('auth_token: TEST2')

    config = ConfigResolver().with_provider_config_file(
        'cloudflare', str(provider_file))

    assert config.resolve('lexicon:cloudflare:auth_token') == 'TEST2'
    assert config.resolve('lexicon:nonexistent') is None
Example #11
0
def test_prioritized_resolution(tmpdir, monkeypatch):
    lexicon_file = tmpdir.join("lexicon.yml")
    lexicon_file.write("cloudflare:\n  auth_token: TEST1")

    monkeypatch.setenv("LEXICON_CLOUDFLARE_AUTH_TOKEN", "TEST2")

    assert (ConfigResolver().with_config_file(
        str(lexicon_file)).with_env().resolve("lexicon:cloudflare:auth_token")
            == "TEST1")
    assert (ConfigResolver().with_env().with_config_file(
        str(lexicon_file)).resolve("lexicon:cloudflare:auth_token") == "TEST2")
Example #12
0
def test_prioritized_resolution(tmpdir, monkeypatch):
    lexicon_file = tmpdir.join('lexicon.yml')
    lexicon_file.write('cloudflare:\n  auth_token: TEST1')

    monkeypatch.setenv('LEXICON_CLOUDFLARE_AUTH_TOKEN', 'TEST2')

    assert ConfigResolver().with_config_file(
        str(lexicon_file)).with_env().resolve(
            'lexicon:cloudflare:auth_token') == 'TEST1'
    assert ConfigResolver().with_env().with_config_file(
        str(lexicon_file)).resolve('lexicon:cloudflare:auth_token') == 'TEST2'
Example #13
0
    def _create_client(self, zone_name):
        config = LexiconConfigResolver()
        dynamic_config = OnTheFlyLexiconConfigSource(zone_name)

        config.with_config_source(dynamic_config) \
            .with_env().with_dict(self.lexicon_config)

        try:
            return LexiconClient(config), dynamic_config
        except AttributeError as e:
            self.log.error('Unable to parse config {!s}'.format(config))
            raise e
Example #14
0
def test_argparse_resolution():
    parser = generate_cli_main_parser()
    data = parser.parse_args([
        '--delegated', 'TEST1', 'cloudflare', 'create', 'example.com', 'TXT',
        '--auth-token', 'TEST2'
    ])

    config = ConfigResolver().with_args(data)

    assert config.resolve('lexicon:delegated') == 'TEST1'
    assert config.resolve('lexicon:cloudflare:auth_token') == 'TEST2'
    assert config.resolve('lexicon:nonexistent') is None
Example #15
0
def txt_challenge(
    certificate: Dict[str, Any],
    profile: Dict[str, Any],
    token: str,
    domain: str,
    action: str = "create",
):
    profile_name = profile["name"]
    provider_name = profile["provider"]
    provider_options = profile.get("provider_options", {})

    if not provider_options:
        print(f"No provider_options are defined for profile {profile_name}, "
              "any call to the provider API is likely to fail.")

    challenge_name = f"_acme-challenge.{domain}."
    if certificate.get("follow_cnames"):
        print(
            f"Trying to resolve the canonical challenge name for {challenge_name}"
        )
        canonical_challenge_name = resolve_canonical_challenge_name(
            challenge_name)
        print(
            f"Canonical challenge name found for {challenge_name}: {canonical_challenge_name}"
        )
        challenge_name = canonical_challenge_name

        extracted = tldextract.extract(challenge_name)
        domain = ".".join([extracted.domain, extracted.suffix])

    config_dict = {
        "action": action,
        "domain": domain,
        "type": "TXT",
        "name": challenge_name,
        "content": token,
        "delegated": profile.get("delegated_subdomain"),
        "provider_name": provider_name,
        provider_name: provider_options,
    }

    ttl = profile.get("ttl")
    if ttl:
        config_dict["ttl"] = ttl

    lexicon_config = ConfigResolver()
    lexicon_config.with_dict(config_dict)

    Client(lexicon_config).execute()
Example #16
0
def test_delete_action_is_correctly_handled_by_provider(
        capsys, lexicon_client):
    client = lexicon_client.Client(ConfigResolver().with_dict({
        "action":
        "delete",
        "provider_name":
        "fakeprovider",
        "domain":
        "example.com",
        "identifier":
        "fake-id",
        "type":
        "TXT",
        "name":
        "fake",
        "content":
        "fake-content",
    }))
    results = client.execute()

    out, _ = capsys.readouterr()

    assert "Authenticate action" in out
    assert results["action"] == "delete"
    assert results["domain"] == "example.com"
    assert results["identifier"] == "fake-id"
    assert results["type"] == "TXT"
    assert results["name"] == "fake"
    assert results["content"] == "fake-content"
Example #17
0
 def add_dns_record(self, record):
     """
     Add a DNS record to the domain.
     """
     lexicon_config = self._get_base_config()
     lexicon_config['domain'] = record['domain']
     lexicon_config['action'] = 'create'
     lexicon_config['type'] = record['type']
     lexicon_config['name'] = record['name']
     lexicon_config['content'] = record['value']
     lexicon_config['ttl'] = record['ttl']
     config = ConfigResolver()
     config.with_dict(dict_object=lexicon_config)
     client = Client(config)
     result = client.execute()
     return result
def build_lexicon_config(
    lexicon_provider_name: str, lexicon_options: Mapping[str, Any],
    provider_options: Mapping[str,
                              Any]) -> Union[ConfigResolver, Dict[str, Any]]:
    """
    Convenient function to build a Lexicon 2.x/3.x config object.
    :param str lexicon_provider_name: the name of the lexicon provider to use
    :param dict lexicon_options: options specific to lexicon
    :param dict provider_options: options specific to provider
    :return: configuration to apply to the provider
    :rtype: ConfigurationResolver or dict
    """
    config: Union[ConfigResolver, Dict[str, Any]] = {
        'provider_name': lexicon_provider_name
    }
    config.update(lexicon_options)
    if not ConfigResolver:
        # Lexicon 2.x
        config.update(provider_options)
    else:
        # Lexicon 3.x
        provider_config: Dict[str, Any] = {}
        provider_config.update(provider_options)
        config[lexicon_provider_name] = provider_config
        config = ConfigResolver().with_dict(config).with_env()

    return config
Example #19
0
def test_client_init_when_missing_type_should_fail():
    options = {
        "provider_name": "fakeprovider",
        "action": "list",
        "domain": "example.com",
    }
    with pytest.raises(AttributeError):
        lexicon.client.Client(ConfigResolver().with_dict(options))
Example #20
0
def test_argparse_resolution():
    parser = generate_cli_main_parser()
    data = parser.parse_args([
        "--delegated",
        "TEST1",
        "cloudflare",
        "create",
        "example.com",
        "TXT",
        "--auth-token",
        "TEST2",
    ])

    config = ConfigResolver().with_args(data)

    assert config.resolve("lexicon:delegated") == "TEST1"
    assert config.resolve("lexicon:cloudflare:auth_token") == "TEST2"
    assert config.resolve("lexicon:nonexistent") is None
Example #21
0
def test_missing_optional_client_config_parameter_does_not_raise_error(
        lexicon_client):
    lexicon_client.Client(ConfigResolver().with_dict({
        "action": "list",
        "provider_name": "fakeprovider",
        "domain": "example.com",
        "type": "TXT",
        "no-name": "fake",
        "no-content": "fake",
    }))
Example #22
0
def test_unknown_provider_raises_error(lexicon_client):
    with pytest.raises(ProviderNotAvailableError):
        lexicon_client.Client(ConfigResolver().with_dict({
            "action": "list",
            "provider_name": "unknownprovider",
            "domain": "example.com",
            "type": "TXT",
            "name": "fake",
            "content": "fake",
        }))
Example #23
0
    def authenticate(self):
        """
        Launch the authentication process: for 'auto' provider, it means first to find the relevant
        provider, then call its authenticate() method. Almost every subsequent operation will then 
        be delegated to that provider.
        """
        mapping_override = self.config.resolve('lexicon:auto:mapping_override')
        print(mapping_override)
        mapping_override_processed = {}
        if mapping_override:
            for one_mapping in mapping_override.split(','):
                one_mapping_processed = one_mapping.split(':')
                mapping_override_processed[
                    one_mapping_processed[0]] = one_mapping_processed[1]

        override_provider = mapping_override_processed.get(self.domain)
        if override_provider:
            provider = [
                element for element in AVAILABLE_PROVIDERS.items()
                if element[0] == override_provider
            ][0]
            LOGGER.info('Provider authoritatively mapped for domain %s: %s.',
                        self.domain, provider.__name__)
            (provider_name, provider_module) = provider
        else:
            (provider_name,
             provider_module) = _relevant_provider_for_domain(self.domain)
            LOGGER.info('Provider discovered for domain %s: %s.', self.domain,
                        provider_name)

        new_config = ConfigResolver()
        new_config.with_dict({'lexicon:provider_name': provider_name})

        target_prefix = 'auto_{0}_'.format(provider_name)
        for configSource in self.config._config_sources:
            if not isinstance(configSource, ArgsConfigSource):
                new_config.with_config_source(configSource)
            else:
                # ArgsConfigSource needs to be reprocessed to rescope the provided
                # args to the delegate provider
                new_dict = {}
                for key, value in configSource._parameters.items():
                    if key.startswith(target_prefix):
                        new_param_name = re.sub('^{0}'.format(target_prefix),
                                                '', key)
                        new_dict['lexicon:{0}:{1}'.format(
                            provider_name, new_param_name)] = value
                    elif not key.startswith('auto_'):
                        new_dict['lexicon:{0}'.format(key)] = value
                new_config.with_dict(new_dict)

        self.proxy_provider = provider_module.Provider(new_config)
        self.proxy_provider.authenticate()
Example #24
0
    def list_dns_records(self, record):
        """
        List all records of a domain name for a given type.
        """
        cached_result = cache.get(f"{record['domain']}-{record['type']}")
        if cached_result:
            return cached_result

        lexicon_config = self._get_base_config()
        lexicon_config['domain'] = record['domain']
        lexicon_config['action'] = 'list'
        lexicon_config['type'] = record['type']
        config = ConfigResolver()
        config.with_dict(dict_object=lexicon_config)
        client = Client(config)

        result = client.execute()
        cache.set(f"{record['domain']}-{record['type']}", result)

        return result
Example #25
0
def test_client_init_when_domain_includes_subdomain_should_strip():
    options = {
        "provider_name": "fakeprovider",
        "action": "list",
        "domain": "www.example.com",
        "type": "TXT",
    }
    client = lexicon.client.Client(ConfigResolver().with_dict(options))

    assert client.provider_name == options["provider_name"]
    assert client.action == options["action"]
    assert client.config.resolve("lexicon:domain") == "example.com"
    assert client.config.resolve("lexicon:type") == options["type"]
Example #26
0
def test_client_basic_init():
    options = {
        "provider_name": "fakeprovider",
        "action": "list",
        "domain": "example.com",
        "type": "TXT",
    }
    client = lexicon.client.Client(ConfigResolver().with_dict(options))

    assert client.provider_name == options["provider_name"]
    assert client.action == options["action"]
    assert client.config.resolve("lexicon:domain") == options["domain"]
    assert client.config.resolve("lexicon:type") == options["type"]
Example #27
0
def test_Client_basic_init():
    options = {
        'provider_name': 'base',
        'action': 'list',
        'domain': 'example.com',
        'type': 'TXT'
    }
    client = lexicon.client.Client(ConfigResolver().with_dict(options))

    assert client.provider_name == options['provider_name']
    assert client.action == options['action']
    assert client.config.resolve('lexicon:domain') == options['domain']
    assert client.config.resolve('lexicon:type') == options['type']
Example #28
0
def test_client_init_with_same_delegated_domain_fqdn():
    options = {
        "provider_name": "fakeprovider",
        "action": "list",
        "domain": "www.example.com",
        "delegated": "example.com",
        "type": "TXT",
    }
    client = lexicon.client.Client(ConfigResolver().with_dict(options))

    assert client.provider_name == options["provider_name"]
    assert client.action == options["action"]
    assert client.config.resolve("lexicon:domain") == "example.com"
    assert client.config.resolve("lexicon:type") == options["type"]
Example #29
0
def main():
    """Main function of Lexicon."""
    # Dynamically determine all the providers available and gather command line arguments.
    parsed_args = generate_cli_main_parser().parse_args()

    log_level = logging.getLevelName(parsed_args.log_level)
    logging.basicConfig(stream=sys.stdout,
                        level=log_level,
                        format='%(message)s')
    logger.debug('Arguments: %s', parsed_args)

    # In the CLI context, will get configuration interactively:
    #   * from the command line
    #   * from the environment variables
    #   * from lexicon configuration files in working directory
    config = ConfigResolver()
    config.with_args(parsed_args).with_env().with_config_dir(os.getcwd())

    client = Client(config)

    results = client.execute()

    handle_output(results, parsed_args.output)
Example #30
0
def test_missing_required_client_config_parameter_raises_error(lexicon_client):
    with pytest.raises(AttributeError):
        lexicon_client.Client(ConfigResolver().with_dict({
            "no-action": "list",
            "provider_name": "fakeprovider",
            "domain": "example.com",
            "type": "TXT",
            "name": "fake",
            "content": "fake",
        }))
    with pytest.raises(AttributeError):
        lexicon_client.Client(ConfigResolver().with_dict({
            "action": "list",
            "no-provider_name": "fakeprovider",
            "domain": "example.com",
            "type": "TXT",
            "name": "fake",
            "content": "fake",
        }))
    with pytest.raises(AttributeError):
        lexicon_client.Client(ConfigResolver().with_dict({
            "action": "list",
            "provider_name": "fakeprovider",
            "no-domain": "example.com",
            "type": "TXT",
            "name": "fake",
            "content": "fake",
        }))
    with pytest.raises(AttributeError):
        lexicon_client.Client(ConfigResolver().with_dict({
            "action": "list",
            "provider_name": "fakeprovider",
            "domain": "example.com",
            "no-type": "TXT",
            "name": "fake",
            "content": "fake",
        }))