Archived
1

Compare commits

...

2 Commits

Author SHA1 Message Date
Ventilaar
7f434069f4 Readme page met screenshots 2022-04-02 18:26:21 +02:00
Ventilaar
236cadf901 cleanup and comment code 2022-04-02 18:09:33 +02:00
8 changed files with 164 additions and 136 deletions

View File

@@ -15,16 +15,20 @@ parser.add_argument('name')
parser.add_argument('value')
parser.add_argument('type')
# verander de variabelen hieronder als je de script handmatig uitvoert
dnsserver = os.environ.get('DNS_SERVER')
dnsserverport = int(os.environ.get('DNS_PORT', default=53))
debug = bool(os.environ.get('API_DEBUG', default=False))
# end change
if dnsserver is None:
print('You did not set a DNS_SERVER environ')
exit(1)
def check_ipv4(ipv4):
def check_ipv4(ipv4): # check if given ipv4 address in string is a valid one
try:
if type(ip_address(ipv4)) is not IPv4Address: # als ip adress ongelig is
raise ValueError # raise value error
@@ -34,13 +38,7 @@ def check_ipv4(ipv4):
return True # adres correct
def make_fqdn_check(subname, parentdomain):
"""
De functie moet de subdomain naam met de parent domain samenvoegen en daarna op errors controleren
:param subname: hostname
:param parentdomain: zone name
:return: hostname.domain.tld in string
"""
def make_fqdn_check(subname, parentdomain): # make fqdn from given subdomain name, also check if valid
fqdn = f'{subname}.{parentdomain}'
if True: # do some regex checking if it made a correct fqdn, currently passing and trusting user input!!!
return fqdn
@@ -48,27 +46,27 @@ def make_fqdn_check(subname, parentdomain):
return False
def validate_authorization(req):
def validate_authorization(req): # validate authorization header
jwt = req.get('Authorization')
if jwt is None:
return False
jwt = jwt.split(' ')[1]
jwt = jwt.split(' ')[1] # get jwt from header
if GoogleOID.check_jwt(jwt)['error'] is False:
if GoogleOID.check_jwt(jwt)['error'] is False: # check if jwt is valid
return True
return False
class domains(Resource):
def get(self):
def get(self): # list domains
"""
list all domains
:return: {"domains": ["name":"a.b"], "error":false}
"""
# Vooralsnog moet je handmatig domains toevoegen aan BIND, dus dit heeft niet veel nut, hardcoded domeinen.
if validate_authorization(request.headers) is False:
if validate_authorization(request.headers) is False: # check authorization
return {"error": True, "reason": "Invalid authorization header"}, 403
return {'domains': ['school.test'], 'error': False}
@@ -81,7 +79,7 @@ class domain(Resource):
:param fqdn: domain name
:return:
"""
if validate_authorization(request.headers) is False:
if validate_authorization(request.headers) is False: # check authorization
return {"error": True, "reason": "Invalid authorization header"}, 403
args = parser.parse_args()
@@ -94,21 +92,21 @@ class domain(Resource):
if t != 'A':
return {"error": True, "reason": "Only A records are supported"}, 400
fqdn = make_fqdn_check(n, dmn)
fqdn = make_fqdn_check(n, dmn) # combine subdomain with parent domain
if fqdn is False:
return {"error": True, "reason": "Invalid subdomain, domain combination"}, 400
dns = DnsZone(dmn, dnsserver, 1, dnsserverport)
dns = DnsZone(dmn, dnsserver, 1, dnsserverport) # setup dnszone
current = dns.check_address(fqdn)
current = dns.check_address(fqdn) # check if address exists
if current['error'] is True:
if 'not found' not in current['error_text']: # als er een error is wat niet een not found is
if 'not found' not in current['error_text']: # als er een error is wat NIET een "not found" is
return {"error": True, "reason": current['error_text']}, 500
else: # record bestond al niet
return {"error": True, "reason": "Record does not exist"}, 400
new = dns.clear_address(fqdn)
new = dns.clear_address(fqdn) # delete record
if new['error'] is True: # als er een error is met het weghalen van dns regel
return {"error": True, "reason": new['error_text']}, 500
@@ -121,12 +119,11 @@ class domain(Resource):
:param dmn: domain name
:return: alle dns A records als keys
"""
# de dnszone library kan geen lijst met records opvragen, een andere manier uitzoeken? mongodb bijhouden?
if validate_authorization(request.headers) is False:
if validate_authorization(request.headers) is False: # check authorization
return {"error": True, "reason": "Invalid authorization header"}, 403
dns = DnsZone(dmn, dnsserver, 1, dnsserverport)
current = dns.list_addresses()
dns = DnsZone(dmn, dnsserver, 1, dnsserverport) # setup dnszone
current = dns.list_addresses() # get all a records
if current['error'] is True:
return {"error": True, "reason": current['error_text']}, 500
@@ -140,7 +137,7 @@ class domain(Resource):
:param dmn: domain name
:return:
"""
if validate_authorization(request.headers) is False:
if validate_authorization(request.headers) is False: # check authorization
return {"error": True, "reason": "Invalid authorization header"}, 403
args = parser.parse_args()
@@ -153,7 +150,7 @@ class domain(Resource):
if t != 'A': # als record type niet A is
return {"error": True, "reason": "Only A records are supported"}, 400
if check_ipv4(v) is False:
if check_ipv4(v) is False: # check if ip is valid
return {"error": True, "reason": "Value is not correct"}, 400
fqdn = make_fqdn_check(n, dmn) # maak fqdn
@@ -188,8 +185,9 @@ api.add_resource(domains, '/api/v1/dns/domains')
api.add_resource(domain, '/api/v1/dns/domain/<dmn>')
if __name__ == '__main__':
if check_ipv4(dnsserver) is False: # cant use dns in resolver so check if direct ip is given if not
dnsserver = resolver.resolve(dnsserver, "A")[0].to_text() # set try to resolve given string
if check_ipv4(dnsserver) is False: # cant use domain in resolver so check if direct ip is given if not
dnsserver = resolver.resolve(dnsserver, "A")[0].to_text() # try to resolve given string
GoogleOID = GoogleOID()
app.run(debug=debug, host='0.0.0.0', port=5001)
# do not setup dns zone globally because it errors on simultaneous requests
GoogleOID = GoogleOID() # setup google oid
app.run(debug=debug, host='0.0.0.0', port=5001) # run werkzeug

View File

@@ -3,8 +3,9 @@ class GoogleOID:
import requests
import jwt
import os
client_secret = os.environ.get('OPENID_SECRET')
if client_secret is None:
client_secret = os.environ.get('OPENID_SECRET') # change this to your secret if running manually
if client_secret is None: # if environ not set
print('No OPENID_SECRET environ')
exit(1)
@@ -14,47 +15,42 @@ class GoogleOID:
self.settings = {'client_id': '954325872153-1v466clrtgg6h4ptt2ne5pgpb9mhilr5.apps.googleusercontent.com',
'client_secret': client_secret,
'callback_uri': 'http://dns.mashallah.nl:5000/login/gcp/callback',
'key_server': 'https://www.googleapis.com/oauth2/v3/certs'}
'key_server': 'https://www.googleapis.com/oauth2/v3/certs'} # global oid settings
def settings(self):
def settings(self): # make it so that the settings variable is callable
return self.settings
def get_token_from_code(self, code):
def get_token_from_code(self, code): # get usable api token from oauth code
data = {'grant_type': 'authorization_code',
'client_id': self.settings['client_id'],
'client_secret': self.settings['client_secret'],
'redirect_uri': self.settings['callback_uri'],
'code': code}
r = self.requests.post('https://oauth2.googleapis.com/token', data=data)
r = self.requests.post('https://oauth2.googleapis.com/token', data=data) # exchange code for token
if r.status_code != 200:
if r.status_code != 200: # if not successful
return {'error': True, 'reason': f'Could not exchange code for access key'}
return {'error': False, 'data': r.json()}
def get_profile_information(self, token):
def get_profile_information(self, token): # get google profile with token
headers = {'Authorization': f'Bearer {token}'}
r = self.requests.get('https://openidconnect.googleapis.com/v1/userinfo', headers=headers)
if r.status_code != 200:
if r.status_code != 200: # if not successful
return {'error': True, 'reason': 'Could not get profile info'}
return {'error': False, 'profile': r.json()}
def check_jwt(self, bearer):
"""
Decodes JWT and checks if it is for us from google
:param bearer: JWT token in base64 format
:return: {'error': False, 'data': decoded jwt dict}
"""
jwks_client = self.jwt.PyJWKClient(self.settings['key_server'])
signing_key = jwks_client.get_signing_key_from_jwt(bearer).key
def check_jwt(self, bearer): # check if jwt is signed by google and valid
jwks_client = self.jwt.PyJWKClient(self.settings['key_server']) # setup jwks client with keyserver
signing_key = jwks_client.get_signing_key_from_jwt(bearer).key # extract signing key from jwt
try:
try: # fails if jwt is not valid eg. not signed by google, expired, wrong application
decoded = self.jwt.decode(bearer, signing_key, algorithms=["RS256"], audience=self.settings['client_id'])
except self.jwt.exceptions.DecodeError:
except self.jwt.exceptions.DecodeError: # catch generic error
return {'error': True, 'reason': 'Error decoding JWT'}
return {'error': False, 'data': decoded}

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.4 KiB

View File

@@ -0,0 +1,40 @@
# DNS Manager GUI
In deze folder bevind zich alle code omtrent de web-gui voor de DNS manager.
## Install
### Container
Als je direct de container gebruikt moet je de environment variables gebruiken die op de docker page staat
https://hub.docker.com/r/4grxfq/gui/ vergeet ook poort 5000 niet te openen naar je container
### Manual install
1. verander de variabelen in gui.py die aangepast moeten worden of zet de environment variabelen in je shell
2. run ```pip install -r /app/requirements.txt```
3. start flask ```python3 gui.py``` de webserver luistert naar poort 5000
## Screenshots
### Index page
Op de index page wordt er een inlog form getoont, ook worden hyperlinks getoont naar de dashboard voor als je al
ingelogt bent, en een link naar de uitlog pad. De username en password textvakken doen niets en zijn puur cosmetisch,,
je kunt alleen inloggen met de klop die je redirect naar de google OID login pagina.
![login page](.assets/login.png)
### Dashboard page
De dashboard page laat een basale tabel zien met alle A records die op de nameserver ingesteld staan. Bovenin wordt je
profiel getoont zoals gekregen van google en opgeslagen in de database.
Met de knop "Query JWT" kan je de JWT opvragen die google mee heeft gegeven met het inloggen, deze JWT kan je gebruiken om de API handmatig met curl aan te roepen en
is verplicht.
Met de Add SUB form kan je een gebruiker authorizeren om in laten te loggen. Als een gebruikter niet mag inloggen wordt
tijdens het inloggen een error getoont met een 21 cijferig code. Die code moet in de Google UUID box worden ingevoerd
om de desbetreffende gebruiker authorizatie te geven om in te kunnen loggen.
De tabel met DNS records spreekt voorzich. Alle bestaande records worden weergeven en de adressen zijn aanpasbaar.
Wanneer er een adres aangepast moet worden is dat makkelijk te doen door de waarde aan te wijzigen en Update aan te
klikken. Evendeels is het verwijderen van een record ook mogelijk door op Delete te klikken.
Een nieuwe record kan worden toegevoegd door de subdomain aan te geven met de juiste IPv4 Waarde. Momenteel zijn alleen
A records ondersteund. Klik op Add om de record toe te voegen.
![dashboard page](.assets/dashboard.png)

View File

@@ -5,15 +5,20 @@ from mango import Mango
from openid import *
import os
# the options below you must change if you want to use a different zone and or want to run the script manually
zone = 'school.test'
dns_api = os.environ.get('DNS_API')
nosql_url = os.environ.get('NOSQL_URL')
nosql_user = os.environ.get('NOSQL_USER')
nosql_pass = os.environ.get('NOSQL_PASS')
nosql_port = int(os.environ.get('NOSQL_PORT', default=27017))
debug = bool(os.environ.get('API_DEBUG', default=False))
nosql_port = int(os.environ.get('NOSQL_PORT', default=27017)) # get NOSQL_PORT as int
debug = bool(os.environ.get('API_DEBUG', default=False)) # debug option as bool
# end editing
if dns_api is None or nosql_pass is None or nosql_port is None or nosql_user is None or nosql_url is None:
# check if vars are not set
print('Missing DNS or NOSQL environs')
exit(1)
@@ -21,14 +26,14 @@ app = Flask(__name__)
app.secret_key = secrets.token_hex(22)
def record_update(name, typ, value, jwt):
def record_update(name, typ, value, jwt): # update record via api server
data = {'name': name, 'type': typ, 'value': value}
headers = {'Authorization': f'Bearer {jwt}'}
r = requests.post(f'{dns_api}/api/v1/dns/domain/{zone}', data=data, headers=headers)
if r.status_code != 200:
return {'error': True, 'reason': f'Request got status code: {r.status_code}'}
if r.status_code != 200: # if not sucessful
return {'error': True, 'reason': f'Request got status code: {r.status_code}'} # return status code from api
return {'error': False}
@@ -37,9 +42,10 @@ def records_get(jwt):
headers = {'Authorization': f'Bearer {jwt}'}
r = requests.get(f'{dns_api}/api/v1/dns/domain/{zone}', headers=headers)
if r.status_code != 200:
return {'error': True, 'reason': f'Request got status code: {r.status_code}'}
return r.json()['records']
if r.status_code != 200: # if not sucessful
return {'error': True, 'reason': f'Request got status code: {r.status_code}'} # return status code from api
return r.json()['records'] # return all the records as a dict
def record_delete(name, jwt):
@@ -47,18 +53,20 @@ def record_delete(name, jwt):
headers = {'Authorization': f'Bearer {jwt}'}
r = requests.delete(f'{dns_api}/api/v1/dns/domain/{zone}', data=data, headers=headers)
if r.status_code != 200:
return {'error': True, 'reason': f'Request got status code: {r.status_code}'}
if r.status_code != 200: # if not sucessful
return {'error': True, 'reason': f'Request got status code: {r.status_code}'} # return status code from api
return {'error': False}
@app.route('/')
def index():
def index(): # base index page
return render_template('index.html')
@app.route('/login/gcp/start')
def login_start():
def login_start(): # client gets redirected to google login page with parameters
nonce = secrets.token_hex(16)
return redirect(f'https://accounts.google.com/o/oauth2/v2/auth?'
f'response_type=code'
@@ -69,100 +77,90 @@ def login_start():
@app.route('/login/gcp/callback')
def login_callback():
"""
We get parameters:(code)
:return:
"""
code = request.args.get('code')
def login_callback(): # client gets returned from google with parameters
code = request.args.get('code') # get the code given by google
if code is None:
return 'Did not get correct parameters. Missing code'
grant = GoogleOID.get_token_from_code(code)
grant = GoogleOID.get_token_from_code(code) # exchange code for access token
if grant['error'] is True:
return f"Could not exchange code for token: {grant['reason']}"
profile = GoogleOID.get_profile_information(grant['data']['access_token'])
profile = GoogleOID.get_profile_information(grant['data']['access_token']) # get profile info from google
if profile['error'] is True:
return f"Could not get profile information: {profile['reason']}"
if db.google_check_sso_uuid(profile['profile']['sub'])['error'] is True:
if db.google_check_sso_uuid(profile['profile']['sub'])['error'] is True: # check if user may login
return f"Account is unavailable for login {profile['profile']['sub']}"
db.google_update_profile(profile['profile'])
db.google_update_lastlogin(profile['profile']['sub'])
db.google_overwrite_jwt(profile['profile']['sub'], grant['data']['id_token'])
db.google_update_profile(profile['profile']) # update google profile in db
db.google_update_lastlogin(profile['profile']['sub']) # change login date in db
db.google_overwrite_jwt(profile['profile']['sub'], grant['data']['id_token']) # overwrite jwt in db
session['username'] = profile['profile']['sub']
session['username'] = profile['profile']['sub'] # set flask session so user gets logged in
return redirect(url_for('dashboard'))
return redirect(url_for('dashboard')) # redirect to dashboard
@app.route('/dashboard', methods=['GET', 'POST'])
def dashboard():
if 'username' not in session:
def dashboard(): # dns manager dashboard
if 'username' not in session: # check if flask session is set(user logged in)
return 'You are not logged in <a href="/">login</a>'
if request.method == 'POST':
if request.method == 'POST': # if form posted
na = request.form.get('name')
ty = request.form.get('type')
va = request.form.get('value')
rq = request.form.get('request')
if rq != "Add" and rq != "Delete" and rq != "Update" and rq != "Query JWT" and rq != "Add SUB":
# data is missing
return 'Invalid request, did you use the dashboard?'
if rq == 'Add' or rq == 'Update':
jwt = db.google_get_jwt(session['username'])
if rq == 'Add' or rq == 'Update': # if user adds or updates a record
jwt = db.google_get_jwt(session['username']) # get jwt from db
response = record_update(name=na, typ=ty, value=va, jwt=jwt)
response = record_update(name=na, typ=ty, value=va, jwt=jwt) # update record
if response['error'] is True:
if response['error'] is True: # if error with update record
return f"Error processing request: {response['reason']}"
elif rq == 'Delete':
jwt = db.google_get_jwt(session['username'])
jwt = db.google_get_jwt(session['username']) # get jwt from db
response = record_delete(name=na, jwt=jwt)
response = record_delete(name=na, jwt=jwt) # delete record
if response['error'] is True:
if response['error'] is True: # if error with delete record
return f"Error processing request: {response['reason']}"
elif rq == "Query JWT":
jwt = db.google_get_jwt(session['username'])
flash(jwt)
elif rq == "Query JWT": # if user requests jwt from db
jwt = db.google_get_jwt(session['username']) # get jwt from db
flash(jwt) # flash jwt message
elif rq == "Add SUB":
db.google_add_new_sub(va)
elif rq == "Add SUB": # user adds a sub
db.google_add_new_sub(va) # add sub to db
uuid = session['username']
profile = db.google_get_profile(uuid)
lastlogin = db.google_get_lastlogin(uuid)
jwt = db.google_get_jwt(uuid)
records = records_get(jwt)
# the functions below are always returned even if POST is used this will keep the page the same after a POST request
uuid = session['username'] # get uuid from session
profile = db.google_get_profile(uuid) # get profile from db
lastlogin = db.google_get_lastlogin(uuid) # get lastlogin from db
jwt = db.google_get_jwt(uuid) # get jwt from db
records = records_get(jwt) # request records from dns api
return render_template('dashboard.html', profile=profile, records=records, lastlogin=lastlogin)
@app.route('/logout')
def logout():
session.pop('username', None)
def logout(): # user logs out
session.pop('username', None) # remove flask session
return redirect(url_for('index'))
@app.route('/login/gcp/addsub', methods=['POST'])
def addsub():
if 'username' not in session:
return 'You are not logged in <a href="/">login</a>'
db.google_add_new_sub(request.form.get('sub'))
return 'sub toegevoegd, gebruiker mag volgende keer inloggen'
if __name__ == '__main__':
db = Mango(nosql_user, nosql_pass, nosql_url, nosql_port)
GoogleOID = GoogleOID()
app.run(debug=debug, host='0.0.0.0')
db = Mango(nosql_user, nosql_pass, nosql_url, nosql_port) # setup mango
GoogleOID = GoogleOID() # initialize googleoid
app.run(debug=debug, host='0.0.0.0') # run werkzeug

View File

@@ -10,15 +10,15 @@ class Mango:
except ConnectionError:
print('MongoDB connection error')
def google_check_sso_uuid(self, uuid):
def google_check_sso_uuid(self, uuid): # checks if uuid exist in any document
found = self.users.find_one({"sso.google.profile.sub": uuid})
if found is None:
if found is None: # None if nothing found
return {"error": True, "reason": "User not found"}
else:
return {"error": False}
def google_update_lastlogin(self, uuid):
def google_update_lastlogin(self, uuid): # replaces lastlogin with current time for given user
found = self.users.find_one({"sso.google.profile.sub": uuid})
if found is None:
@@ -30,7 +30,7 @@ class Mango:
return {"error": False}
def google_update_profile(self, profile):
def google_update_profile(self, profile): # overwrites user profile with the one that google has given
found = self.users.find_one({"sso.google.profile.sub": profile['sub']})
found['sso']['google']['profile'] = profile
@@ -39,23 +39,23 @@ class Mango:
return {"error": False}
def google_get_profile(self, uuid):
def google_get_profile(self, uuid): # returns google profile as stored in db for a given user
found = self.users.find_one(({"sso.google.profile.sub": uuid}))
return found['sso']['google']['profile']
def google_get_lastlogin(self, uuid):
def google_get_lastlogin(self, uuid): # returns lastlogin in pretty format for given user
found = self.users.find_one(({"sso.google.profile.sub": uuid}))
return found['sso']['google']['lastlogin'].strftime('%A %d-%m-%Y, %H:%M:%S')
def google_add_new_sub(self, uuid):
def google_add_new_sub(self, uuid): # adds new document in db with only the sub for a given uuid
self.users.insert_one({'sso':{'google':{'profile':{"sub": str(uuid)}}}})
def google_overwrite_jwt(self, uuid, jwt):
def google_overwrite_jwt(self, uuid, jwt): # overwrite jwt in db for given user
found = self.users.find_one({"sso.google.profile.sub": uuid})
found['sso']['google']['jwt'] = jwt
self.users.replace_one({"sso.google.profile.sub": uuid}, found)
def google_get_jwt(self, uuid):
def google_get_jwt(self, uuid): # return jwt as stored in db for given user
found = self.users.find_one({"sso.google.profile.sub": uuid})
return found['sso']['google']['jwt']

View File

@@ -3,8 +3,9 @@ class GoogleOID:
import requests
import jwt
import os
client_secret = os.environ.get('OPENID_SECRET')
if client_secret is None:
client_secret = os.environ.get('OPENID_SECRET') # change this to your secret if running manually
if client_secret is None: # if environ not set
print('No OPENID_SECRET environ')
exit(1)
@@ -14,47 +15,42 @@ class GoogleOID:
self.settings = {'client_id': '954325872153-1v466clrtgg6h4ptt2ne5pgpb9mhilr5.apps.googleusercontent.com',
'client_secret': client_secret,
'callback_uri': 'http://dns.mashallah.nl:5000/login/gcp/callback',
'key_server': 'https://www.googleapis.com/oauth2/v3/certs'}
'key_server': 'https://www.googleapis.com/oauth2/v3/certs'} # global oid settings
def settings(self):
def settings(self): # make it so that the settings variable is callable
return self.settings
def get_token_from_code(self, code):
def get_token_from_code(self, code): # get usable api token from oauth code
data = {'grant_type': 'authorization_code',
'client_id': self.settings['client_id'],
'client_secret': self.settings['client_secret'],
'redirect_uri': self.settings['callback_uri'],
'code': code}
r = self.requests.post('https://oauth2.googleapis.com/token', data=data)
r = self.requests.post('https://oauth2.googleapis.com/token', data=data) # exchange code for token
if r.status_code != 200:
if r.status_code != 200: # if not successful
return {'error': True, 'reason': f'Could not exchange code for access key'}
return {'error': False, 'data': r.json()}
def get_profile_information(self, token):
def get_profile_information(self, token): # get google profile with token
headers = {'Authorization': f'Bearer {token}'}
r = self.requests.get('https://openidconnect.googleapis.com/v1/userinfo', headers=headers)
if r.status_code != 200:
if r.status_code != 200: # if not successful
return {'error': True, 'reason': 'Could not get profile info'}
return {'error': False, 'profile': r.json()}
def check_jwt(self, bearer):
"""
Decodes JWT and checks if it is for us from google
:param bearer: JWT token in base64 format
:return: {'error': False, 'data': decoded jwt dict}
"""
jwks_client = self.jwt.PyJWKClient(self.settings['key_server'])
signing_key = jwks_client.get_signing_key_from_jwt(bearer).key
def check_jwt(self, bearer): # check if jwt is signed by google and valid
jwks_client = self.jwt.PyJWKClient(self.settings['key_server']) # setup jwks client with keyserver
signing_key = jwks_client.get_signing_key_from_jwt(bearer).key # extract signing key from jwt
try:
try: # fails if jwt is not valid eg. not signed by google, expired, wrong application
decoded = self.jwt.decode(bearer, signing_key, algorithms=["RS256"], audience=self.settings['client_id'])
except self.jwt.exceptions.DecodeError:
except self.jwt.exceptions.DecodeError: # catch generic error
return {'error': True, 'reason': 'Error decoding JWT'}
return {'error': False, 'data': decoded}