1
0
mirror of https://github.com/github/octodns.git synced 2024-05-11 05:55:00 +00:00

allow to pass login/password which will be used to acquire token for further usage

This commit is contained in:
Yaroshevich, Denis
2021-04-20 14:33:42 +03:00
parent 988e8d27fb
commit 0ff933244a
2 changed files with 137 additions and 39 deletions

View File

@@ -11,7 +11,9 @@ from __future__ import (
from collections import defaultdict from collections import defaultdict
from requests import Session from requests import Session
import http
import logging import logging
import urllib.parse
from ..record import Record from ..record import Record
from .base import BaseProvider from .base import BaseProvider
@@ -34,51 +36,82 @@ class GCoreClientNotFound(GCoreClientException):
class GCoreClient(object): class GCoreClient(object):
ROOT_ZONES = "/zones" ROOT_ZONES = "zones"
def __init__(self, log, base_url, token): def __init__(
session = Session() self,
session.headers.update({"Authorization": "Bearer {}".format(token)}) log,
api_url,
auth_url,
token=None,
login=None,
password=None,
):
self.log = log self.log = log
self._session = session self._session = Session()
self._base_url = base_url self._api_url = api_url
if token is not None:
self._session.headers.update(
{"Authorization": "APIKey {}".format(token)}
)
elif login is not None and password is not None:
token = self._auth(auth_url, login, password)
self._session.headers.update(
{"Authorization": "Bearer {}".format(token)}
)
else:
raise ValueError("either token or login & password must be set")
def _request(self, method, path, params={}, data=None): def _auth(self, url, login, password):
url = "{}{}".format(self._base_url, path) # well, can't use _request, since API returns 400 if credentials
# invalid which will be logged, but we don't want do this
r = self._session.request(
"POST",
self._build_url(url, "auth", "jwt", "login"),
json={"username": login, "password": password},
)
r.raise_for_status()
return r.json()["access"]
def _request(self, method, url, params=None, data=None):
r = self._session.request( r = self._session.request(
method, url, params=params, json=data, timeout=30.0 method, url, params=params, json=data, timeout=30.0
) )
if r.status_code == 400: if r.status_code == http.HTTPStatus.BAD_REQUEST:
self.log.error( self.log.error(
"bad request %r has been sent to %r: %s", data, url, r.text "bad request %r has been sent to %r: %s", data, url, r.text
) )
raise GCoreClientBadRequest(r) raise GCoreClientBadRequest(r)
elif r.status_code == 404: elif r.status_code == http.HTTPStatus.NOT_FOUND:
self.log.error( self.log.error("resource %r not found: %s", url, r.text)
"resource %r not found: %s", url, r.text
)
raise GCoreClientNotFound(r) raise GCoreClientNotFound(r)
elif r.status_code == 500: elif r.status_code == http.HTTPStatus.INTERNAL_SERVER_ERROR:
self.log.error( self.log.error("server error no %r to %r: %s", data, url, r.text)
"server error no %r to %r: %s", data, url, r.text
)
raise GCoreClientException(r) raise GCoreClientException(r)
r.raise_for_status() r.raise_for_status()
return r return r
def zone(self, zone_name): def zone(self, zone_name):
return self._request( return self._request(
"GET", "{}/{}".format(self.ROOT_ZONES, zone_name) "GET", self._build_url(self._api_url, self.ROOT_ZONES, zone_name)
).json() ).json()
def zone_create(self, zone_name): def zone_create(self, zone_name):
return self._request( return self._request(
"POST", self.ROOT_ZONES, data={"name": zone_name} "POST",
self._build_url(self._api_url, self.ROOT_ZONES),
data={"name": zone_name},
).json() ).json()
def zone_records(self, zone_name): def zone_records(self, zone_name):
rrsets = self._request( rrsets = self._request(
"GET", "{}/{}/rrsets?all=true".format(self.ROOT_ZONES, zone_name) "GET",
"{}".format(
self._build_url(
self._api_url, self.ROOT_ZONES, zone_name, "rrsets"
)
),
params={"all": "true"},
).json() ).json()
records = rrsets["rrsets"] records = rrsets["rrsets"]
return records return records
@@ -97,10 +130,17 @@ class GCoreClient(object):
self._request("DELETE", self._rrset_url(zone_name, rrset_name, type_)) self._request("DELETE", self._rrset_url(zone_name, rrset_name, type_))
def _rrset_url(self, zone_name, rrset_name, type_): def _rrset_url(self, zone_name, rrset_name, type_):
return "{}/{}/{}/{}".format( return self._build_url(
self.ROOT_ZONES, zone_name, rrset_name, type_ self._api_url, self.ROOT_ZONES, zone_name, rrset_name, type_
) )
@staticmethod
def _build_url(base, *items):
for i in items:
base = base.strip("/") + "/"
base = urllib.parse.urljoin(base, i)
return base
class GCoreProvider(BaseProvider): class GCoreProvider(BaseProvider):
""" """
@@ -108,8 +148,12 @@ class GCoreProvider(BaseProvider):
gcore: gcore:
class: octodns.provider.gcore.GCoreProvider class: octodns.provider.gcore.GCoreProvider
# Your API key (required) # Your API key
token: XXXXXXXXXXXX token: XXXXXXXXXXXX
# or login + password
login: XXXXXXXXXXXX
password: XXXXXXXXXXXX
# auth_url: https://api.gcdn.co
# url: https://dnsapi.gcorelabs.com/v2 # url: https://dnsapi.gcorelabs.com/v2
""" """
@@ -117,12 +161,18 @@ class GCoreProvider(BaseProvider):
SUPPORTS_DYNAMIC = False SUPPORTS_DYNAMIC = False
SUPPORTS = set(("A", "AAAA")) SUPPORTS = set(("A", "AAAA"))
def __init__(self, id, token, *args, **kwargs): def __init__(self, id, *args, **kwargs):
base_url = kwargs.pop("url", "https://dnsapi.gcorelabs.com/v2") token = kwargs.pop("token", None)
login = kwargs.pop("login", None)
password = kwargs.pop("password", None)
api_url = kwargs.pop("url", "https://dnsapi.gcorelabs.com/v2")
auth_url = kwargs.pop("auth_url", "https://api.gcdn.co")
self.log = logging.getLogger("GCoreProvider[{}]".format(id)) self.log = logging.getLogger("GCoreProvider[{}]".format(id))
self.log.debug("__init__: id=%s, token=***", id) self.log.debug("__init__: id=%s", id)
super(GCoreProvider, self).__init__(id, *args, **kwargs) super(GCoreProvider, self).__init__(id, *args, **kwargs)
self._client = GCoreClient(self.log, base_url, token) self._client = GCoreClient(
self.log, api_url, auth_url, token, login, password
)
def _data_for_single(self, _type, record): def _data_for_single(self, _type, record):
return { return {

View File

@@ -33,7 +33,7 @@ class TestGCoreProvider(TestCase):
def test_populate(self): def test_populate(self):
provider = GCoreProvider("test_id", "token") provider = GCoreProvider("test_id", token="token")
# 400 - Bad Request. # 400 - Bad Request.
with requests_mock() as mock: with requests_mock() as mock:
@@ -52,7 +52,7 @@ class TestGCoreProvider(TestCase):
with self.assertRaises(GCoreClientNotFound) as ctx: with self.assertRaises(GCoreClientNotFound) as ctx:
zone = Zone("unit.tests.", []) zone = Zone("unit.tests.", [])
provider._client.zone(zone) provider._client.zone(zone.name)
self.assertIn( self.assertIn(
'"error":"zone is not found"', text_type(ctx.exception) '"error":"zone is not found"', text_type(ctx.exception)
) )
@@ -66,6 +66,48 @@ class TestGCoreProvider(TestCase):
provider.populate(zone) provider.populate(zone)
self.assertEquals("Things caught fire", text_type(ctx.exception)) self.assertEquals("Things caught fire", text_type(ctx.exception))
# No credentials or token error
with requests_mock() as mock:
with self.assertRaises(ValueError) as ctx:
GCoreProvider("test_id")
self.assertEquals(
"either token or login & password must be set",
text_type(ctx.exception),
)
# Auth with login password
with requests_mock() as mock:
def match_body(request):
return {"username": "foo", "password": "bar"} == request.json()
auth_url = "http://api/auth/jwt/login"
mock.post(
auth_url,
additional_matcher=match_body,
status_code=200,
json={"access": "access"},
)
providerPassword = GCoreProvider(
"test_id",
url="http://dns",
auth_url="http://api",
login="foo",
password="bar",
)
assert mock.called
# make sure token passed in header
zone_rrset_url = "http://dns/zones/unit.tests/rrsets?all=true"
mock.get(
zone_rrset_url,
request_headers={"Authorization": "Bearer access"},
status_code=404,
)
zone = Zone("unit.tests.", [])
assert not providerPassword.populate(zone)
# No diffs == no changes # No diffs == no changes
with requests_mock() as mock: with requests_mock() as mock:
base = "https://dnsapi.gcorelabs.com/v2/zones/unit.tests/rrsets" base = "https://dnsapi.gcorelabs.com/v2/zones/unit.tests/rrsets"
@@ -97,7 +139,7 @@ class TestGCoreProvider(TestCase):
) )
def test_apply(self): def test_apply(self):
provider = GCoreProvider("test_id", "token") provider = GCoreProvider("test_id", url="http://api", token="token")
# Zone does not exists but can be created. # Zone does not exists but can be created.
with requests_mock() as mock: with requests_mock() as mock:
@@ -153,12 +195,16 @@ class TestGCoreProvider(TestCase):
provider._client._request.assert_has_calls( provider._client._request.assert_has_calls(
[ [
call("GET", "/zones/unit.tests/rrsets?all=true"), call(
call("GET", "/zones/unit.tests"), "GET",
call("POST", "/zones", data={"name": "unit.tests"}), "http://api/zones/unit.tests/rrsets",
params={"all": "true"},
),
call("GET", "http://api/zones/unit.tests"),
call("POST", "http://api/zones", data={"name": "unit.tests"}),
call( call(
"POST", "POST",
"/zones/unit.tests/www.sub.unit.tests./A", "http://api/zones/unit.tests/www.sub.unit.tests./A",
data={ data={
"ttl": 300, "ttl": 300,
"resource_records": [{"content": ["2.2.3.6"]}], "resource_records": [{"content": ["2.2.3.6"]}],
@@ -166,7 +212,7 @@ class TestGCoreProvider(TestCase):
), ),
call( call(
"POST", "POST",
"/zones/unit.tests/www.unit.tests./A", "http://api/zones/unit.tests/www.unit.tests./A",
data={ data={
"ttl": 300, "ttl": 300,
"resource_records": [{"content": ["2.2.3.6"]}], "resource_records": [{"content": ["2.2.3.6"]}],
@@ -174,7 +220,7 @@ class TestGCoreProvider(TestCase):
), ),
call( call(
"POST", "POST",
"/zones/unit.tests/aaaa.unit.tests./AAAA", "http://api/zones/unit.tests/aaaa.unit.tests./AAAA",
data={ data={
"ttl": 600, "ttl": 600,
"resource_records": [ "resource_records": [
@@ -188,7 +234,7 @@ class TestGCoreProvider(TestCase):
), ),
call( call(
"POST", "POST",
"/zones/unit.tests/unit.tests./A", "http://api/zones/unit.tests/unit.tests./A",
data={ data={
"ttl": 300, "ttl": 300,
"resource_records": [ "resource_records": [
@@ -239,10 +285,12 @@ class TestGCoreProvider(TestCase):
provider._client._request.assert_has_calls( provider._client._request.assert_has_calls(
[ [
call("DELETE", "/zones/unit.tests/www.unit.tests./A"), call(
"DELETE", "http://api/zones/unit.tests/www.unit.tests./A"
),
call( call(
"PUT", "PUT",
"/zones/unit.tests/ttl.unit.tests./A", "http://api/zones/unit.tests/ttl.unit.tests./A",
data={ data={
"ttl": 300, "ttl": 300,
"resource_records": [{"content": ["3.2.3.4"]}], "resource_records": [{"content": ["3.2.3.4"]}],