def new_blob_client() -> BlobClient:
    """
    Returns an instance of BlobClient that attaches to
    state storage blob in Azure storage account.
    """
    return BlobClient.from_connection_string(
        envconfig.req("AzureWebJobsStorage"),
        container_name="statestorage",
        blob_name="state.json",
    )
 def _get_secret_from_keyvault(self, id: str) -> typing.Union[KeyVaultSecret, None]:
     """
     Gets a certificate from Azure Keyvault and returns an instance of KeyVaultSecret. Or if no secret is found returns None.
     """
     logging.debug("Trying to get certificate from Azure KeyVault for '%s'", id)
     vault_name = envconfig.req("AZURE_KEYVAULT_NAME")
     secret_client = SecretClient(
         vault_url=f"https://{vault_name}.vault.azure.net/",
         credential=self.credentials,
     )
     try:
         secret = secret_client.get_secret(id)
         return secret
     except ResourceNotFoundError:
         logging.debug("No certificate found from Azure Keyvault for '%s'", id)
         return None
def main(req: func.HttpRequest, msg: func.Out[str]) -> func.HttpResponse:
    """
    Insert-Update request endpoint
    """
    api_baseurl = envconfig.req("API_BASEURL")
    blob_client = st.new_blob_client()

    logging.debug("CLIENT ID IS %s", req.route_params.get("client_id"))

    # Check if JSON is valid
    try:
        req_json = req.get_json()
    except ValueError:
        return http.invalid_json_response()

    # Validate data
    try:
        req_body = cfg.IngressUpdate(**req_json)
    except pydantic.error_wrappers.ValidationError as e:
        return http.validation_error_response(e)

    ## Check status (if possible, add to queue)
    try:
        client_id = cfg.Id.from_str(req.route_params.get("client_id"))
        state_in_azure = st.StateInAzure(blob_client)
        queue_item = state_in_azure.upsert(client_id, req_body,
                                           datetime.datetime.now())

        msg.set(queue_item.json())
        response = {
            "success": True,
            "msg": "Added to queue.",
            "poll_url": f"{api_baseurl}/v1/{str(client_id)}/status",
            "data": json.loads(queue_item.json()),
        }
        return func.HttpResponse(json.dumps(response),
                                 status_code=202,
                                 headers=http.response_headers())
    except st.UpdateConflictError as e:
        return http.update_conflict_response(e)
    except pydantic.error_wrappers.ValidationError as e:
        return http.validation_error_response(e)
 def from_env(cls: typing.Type["Options"]) -> "Options":
     """
     Load all options from environment variables
     """
     return cls(
         azure_subscription_id=envconfig.req("AZURE_SUBSCRIPTION_ID"),
         azure_dns_zone_resource_group=envconfig.req("AZURE_RG_DNS"),
         azure_dns_zone=envconfig.req("AZURE_DNS_ZONE"),
         azure_keyvault_name=envconfig.req("AZURE_KEYVAULT_NAME"),
         acme_contact_email=envconfig.req("ACME_CONTACT_EMAIL"),
         acme_directory_url=envconfig.req("ACME_DIRECTORY_URL"),
         cert_request_connection_str=envconfig.req("AzureWebJobsStorage"),
         cert_request_container=envconfig.opt(
             "CERT_REQ_CONTAINER", DEFAULT_CERT_REQUESTS_CONTAINER),
         cert_subject=envconfig.opt("CERT_SUBJECT", DEFAULT_CERT_SUBJECT),
         cert_key_size=int(
             envconfig.opt("CERT_KEY_SIZE", str(DEFAULT_CERT_KEY_SIZE))),
         cert_expiry_threshold_days=int(
             envconfig.opt(
                 "CERT_EXPIRY_THRESHOLD_DAYS",
                 str(DEFAULT_CERT_EXPIRY_THRESHOLD_DAYS),
             )),
     )
    def from_env(cls: typing.Type["Options"]) -> "Options":
        """
        Returns a class object of type Options

        Fields: domain, azure (subscription, resource group, dns zone, traffic manager, principal user id)
        plus a list of azure geographical locations where the application is running
        """
        return cls(
            domain=envconfig.req("DOMAIN"),
            azure_subscription_id=envconfig.req("AZURE_SUBSCRIPTION_ID"),
            azure_dns_resource_group=envconfig.req("AZURE_RG_DNS"),
            azure_dns_zone=envconfig.req("AZURE_DNS_ZONE"),
            azure_trafficmanager_resource_group=envconfig.req(
                "AZURE_RG_TRAFFICMANAGER"
            ),
            azure_trafficmanager_name_prefix=envconfig.req(
                "TRAFFICMANAGER_NAME_PREFIX"
            ),
            azure_appgw_managed_identity={
                envconfig.req("AZURE_APPGW_MI"): ManagedIdentityProperties(
                    principal_id=envconfig.req("AZURE_APPGW_MI_PRINCIPAL_ID"),
                    client_id=envconfig.req("AZURE_APPGW_MI_CLIENT_ID"),
                )
            },
            locations=[
                LocationOptions(
                    azure_resource_group_name=envconfig.req("AZURE_APPGW1_RG"),
                    azure_location=envconfig.req("AZURE_APPGW1_LOCATION"),
                    azure_vnet_name=envconfig.req("AZURE_APPGW1_VNET_NAME"),
                    azure_subnet_name=envconfig.req("AZURE_APPGW1_SUBNET_NAME"),
                    azure_subnet_address_prefix=envconfig.req(
                        "AZURE_APPGW1_SUBNET_ADDR_PREFIX"
                    ),
                    backend_address=envconfig.req("AZURE_APPGW1_BACKEND_ADDRESS"),
                    backend_port=int(envconfig.opt("AZURE_APPGW1_BACKEND_PORT", "80")),
                    backend_protocol=envconfig.opt(
                        "AZURE_APPGW1_BACKEND_PROTOCOL", "Http"
                    ),
                ),
                LocationOptions(
                    azure_resource_group_name=envconfig.req("AZURE_APPGW2_RG"),
                    azure_location=envconfig.req("AZURE_APPGW2_LOCATION"),
                    azure_vnet_name=envconfig.req("AZURE_APPGW2_VNET_NAME"),
                    azure_subnet_name=envconfig.req("AZURE_APPGW2_SUBNET_NAME"),
                    azure_subnet_address_prefix=envconfig.req(
                        "AZURE_APPGW2_SUBNET_ADDR_PREFIX"
                    ),
                    backend_address=envconfig.req("AZURE_APPGW2_BACKEND_ADDRESS"),
                    backend_port=int(envconfig.opt("AZURE_APPGW2_BACKEND_PORT", "80")),
                    backend_protocol=envconfig.opt(
                        "AZURE_APPGW2_BACKEND_PROTOCOL", "Http"
                    ),
                ),
            ],
        )
def main(req: func.HttpRequest, msg: func.Out[str]) -> func.HttpResponse:
    api_baseurl = envconfig.req("API_BASEURL")

    # Check if JSON is valid
    try:
        req_json = req.get_json()
    except ValueError:
        return http.invalid_json_response()

    # Validate data
    try:
        req_body = cfg.IngressWhitelistUpdate(**req_json)
    except pydantic.error_wrappers.ValidationError as e:
        return http.validation_error_response(e)

    if len(req_body.ip_whitelist) == 0:
        return func.HttpResponse(
            body=http.response_body(
                success=True,
                msg="No change",
                reason="Provided whitelist was an empty list [].",
            ),
            status_code=200,
            headers=http.response_headers(),
        )

    try:
        # Client for handling state in storage account
        blob_client = st.new_blob_client()

        # Validate client id
        client_id = cfg.Id.from_str(req.route_params.get("client_id"))
        # Get current configuration from state in Azure
        state_in_azure = st.StateInAzure(blob_client)
        state = state_in_azure.get_by_id(client_id)

        # Append all unique ip addresses to the existing whitelist
        for ip in req_body.ip_whitelist:
            if ip not in state.ip_whitelist:
                state.ip_whitelist.append(ip)

        # Create IngressUpdate from new state
        ingress_update = cfg.IngressUpdate(subdomain=state.subdomain,
                                           ip_whitelist=state.ip_whitelist)
        # Update state in Azure and get queue item in return
        queue_item = state_in_azure.upsert(client_id, ingress_update,
                                           datetime.datetime.now())
        msg.set(queue_item.json())
        response = {
            "success": True,
            "msg": "Added to queue.",
            "poll_url": f"{api_baseurl}/v1/{str(client_id)}/status",
            "data": json.loads(state.json()),
        }
        return func.HttpResponse(json.dumps(response),
                                 status_code=202,
                                 headers=http.response_headers())
    except KeyError:
        return http.not_found_response(
            reason=f"Client with id '{client_id}' does not exist")
    except st.UpdateConflictError as e:
        return http.update_conflict_response(e)