Archived
1

Compare commits

...

58 Commits

Author SHA1 Message Date
ventilaar
741ac73e35 redirect to secure site, since it's supported 2022-04-06 12:48:13 +02:00
ventilaar
bb5390928e working kubernetes services, just add the secret variables 2022-04-06 12:33:02 +02:00
ventilaar
e9be4808ed change callback domain and add mango to dockerfile 2022-04-06 12:28:38 +02:00
ventilaar
3b6312b596 change callback domain and add mango to dockerfile 2022-04-06 12:28:24 +02:00
ventilaar
f31221caf0 function serverless works! 2022-04-05 15:31:38 +02:00
ventilaar
f1b0ace7ae terraform works! 2022-04-05 14:02:10 +02:00
ventilaar
c77485c4a1 Merge remote-tracking branch 'origin/master' 2022-04-04 22:23:35 +02:00
ventilaar
755e3be41e works but need to set network security groups straight 2022-04-04 22:23:26 +02:00
Ventilaar
d7dd7b5947 beter gedetailleerder uitgelegd 2022-04-04 22:15:05 +02:00
Ventilaar
4b56c4bd55 created kube deployment of api and gui, just need to test it 2022-04-04 21:59:14 +02:00
Ventilaar
aadfe81674 add required mongo connection string 2022-04-03 13:12:36 +02:00
Ventilaar
265476d24e cloud-init script to download and setup bind vm 2022-04-03 13:09:11 +02:00
Ventilaar
af6fc43067 update test data 2022-04-03 13:08:50 +02:00
Ventilaar
67ed0e0f34 add option to use basic authorization with api key 2022-04-03 13:06:35 +02:00
Ventilaar
ed2e93ed8e change verbosity back 2022-04-02 23:03:35 +02:00
Ventilaar
e9cbcbed42 be more verbose 2022-04-02 23:00:57 +02:00
Ventilaar
85ea999996 be more verbose 2022-04-02 22:56:07 +02:00
Ventilaar
9a50318740 forgot to change class 2022-04-02 22:41:02 +02:00
Ventilaar
0af718949c reflect new mongo connection type 2022-04-02 22:36:10 +02:00
Ventilaar
a0ee4b3d34 no default mongo connection string 2022-04-02 22:34:41 +02:00
Ventilaar
e52b494759 change mongo connection type 2022-04-02 22:33:46 +02:00
Ventilaar
57ae8543f2 add warning, thanks azure 2022-04-02 22:30:21 +02:00
Ventilaar
8941b805d5 update 2022-04-02 22:03:29 +02:00
Ventilaar
05325c2905 change callback domain 2022-04-02 21:50:59 +02:00
Ventilaar
52c9b053be forgor one 2022-04-02 21:49:30 +02:00
Ventilaar
d4e105d980 forgor one 2022-04-02 21:45:55 +02:00
Ventilaar
84b5c1760f change school.test to dns.mashallah.nl 2022-04-02 21:44:36 +02:00
Ventilaar
22f3855e68 formatting 2022-04-02 18:27:55 +02:00
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
Ventilaar
9a70ef5bb1 nice 2022-04-01 13:12:01 +02:00
Ventilaar
c5604df1ca bruh 2022-04-01 13:05:48 +02:00
Ventilaar
2d804e85c9 bruh 2022-03-30 21:36:21 +02:00
Ventilaar
a3bd50613b perfect day to be working on this 2022-03-30 21:34:17 +02:00
Ventilaar
74279b1bd0 suddenly it does not work anymore without this? 2022-03-30 21:26:37 +02:00
Ventilaar
9724c905bf this should work now 2022-03-30 21:13:52 +02:00
Ventilaar
f665628b94 i need coffee 2022-03-30 20:57:32 +02:00
Ventilaar
10971e5e5a idk man test this 2022-03-30 20:48:40 +02:00
Ventilaar
518d90a66f get ipv4 address instead of a response class 2022-03-30 20:46:35 +02:00
Ventilaar
2319dcbfa7 dnsserver mag geen dns adres zijn. resolve eerst naar ipv4 2022-03-30 20:39:31 +02:00
Ventilaar
5b48e994db add debug environ option 2022-03-30 20:26:39 +02:00
Ventilaar
389161bf83 im so stupid with ports 2022-03-30 20:22:28 +02:00
Ventilaar
75133c6452 correct mongo port 2022-03-30 20:20:43 +02:00
Ventilaar
4bfa93c3e4 fml typo 2022-03-30 20:15:45 +02:00
Ventilaar
ecc2ce208d add git pull 2022-03-30 20:13:29 +02:00
Ventilaar
3672b6fa59 simple updater script 2022-03-30 20:13:05 +02:00
Ventilaar
ccfc87937a port int bugfix 2022-03-30 20:09:57 +02:00
Ventilaar
ce3d38f51f add software containers 2022-03-30 20:06:39 +02:00
Ventilaar
695cb09150 moved requirements.txt to same Dockerfile context. Adjusted packages to what is needed 2022-03-30 19:53:06 +02:00
Ventilaar
7b11b0c7f1 more environ requirements and options 2022-03-30 19:48:28 +02:00
Ventilaar
e0b5aceb5b refactor requirements.txt to parent folder, make Dockerfile for gui.py 2022-03-30 19:42:22 +02:00
Ventilaar
041021ab35 little things 2022-03-30 19:22:27 +02:00
Ventilaar
2b0ffb02a4 create pip and dockerfile, disable debugging 2022-03-30 18:59:32 +02:00
Ventilaar
93b5116aa0 get software ready for container deployment 2022-03-30 18:49:27 +02:00
ventilaar
e45632630a change to real dns address 2022-03-21 13:50:17 +01:00
ventilaar
1a7307b89f add option to add sub id to database. and return sub id if account may not login 2022-03-21 13:50:09 +01:00
ventilaar
556b8cbe50 add option to quert jwt from database 2022-03-21 10:31:27 +01:00
ventilaar
844d68b2e3 edit imports and pass trough jwt to functions 2022-03-21 10:20:24 +01:00
28 changed files with 777 additions and 179 deletions

View File

@@ -0,0 +1,10 @@
#cloud-config
bootcmd:
- echo "making directories"
- mkdir -p /etc/bind
- mkdir -p /var/lib/bind
- echo "downloading files"
- wget https://git.ventilaar.nl/ventilaar/clim/raw/branch/master/eindopdracht/testomgeving/bind/named.conf.local -O /etc/bind/named.conf.local
- wget https://git.ventilaar.nl/ventilaar/clim/raw/branch/master/eindopdracht/testomgeving/bind/dns.mashallah.nl.zone -O /var/lib/bind/dns.mashallah.nl.zone
packages:
- bind9

View File

@@ -0,0 +1,63 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: api-deployment
labels:
app: api-deployment
spec:
replicas: 1
selector:
matchLabels:
app: api-deployment
template:
metadata:
labels:
app: api-deployment
spec:
containers:
- name: api-container
image: 4grxfq/api
imagePullPolicy: Always
ports:
- containerPort: 5001
name: api-port
env:
- name: OPENID_SECRET
value:
- name: DNS_SERVER
value: dnsns.mashallah.nl
- name: MONGO_CONNECTIONSTRING
value:
---
apiVersion: v1
kind: Service
metadata:
name: api-service
labels:
run: api-service
spec:
ports:
- port: 80
targetPort: 5001
protocol: TCP
selector:
app: api-deployment
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: api-ingress
annotations:
kubernetes.io/ingress.class: addon-http-application-routing
spec:
rules:
- host: dnsapi.mashallah.nl
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: api-service
port:
number: 80

View File

@@ -0,0 +1,63 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: gui-deployment
labels:
app: gui-deployment
spec:
replicas: 1
selector:
matchLabels:
app: gui-deployment
template:
metadata:
labels:
app: gui-deployment
spec:
containers:
- name: gui-container
image: 4grxfq/gui
imagePullPolicy: Always
ports:
- containerPort: 5000
name: gui-port
env:
- name: OPENID_SECRET
value:
- name: DNS_API
value: http://api-service:80
- name: MONGO_CONNECTIONSTRING
value:
---
apiVersion: v1
kind: Service
metadata:
name: gui-service
labels:
run: gui-service
spec:
ports:
- port: 80
targetPort: 5000
protocol: TCP
selector:
app: gui-deployment
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: gui-ingress
annotations:
kubernetes.io/ingress.class: addon-http-application-routing
spec:
rules:
- host: dnsgui.mashallah.nl
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: gui-service
port:
number: 80

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,11 @@
FROM alpine:latest
RUN apk add --update py3-pip python3-dev gcc libc-dev libffi-dev
RUN pip install --upgrade pip
COPY requirements.txt /app/
RUN pip install --no-cache-dir -r /app/requirements.txt
COPY openid.py /app/
COPY dnszone.py /app/
COPY openid.py /app/
COPY mango.py /app
COPY api.py /app/
CMD ["python3", "/app/api.py"]

View File

@@ -3,7 +3,10 @@ from flask import Flask, request
from flask_restful import reqparse, Api, Resource
from dnszone import DnsZone
from ipaddress import ip_address, IPv4Address
from eindopdracht.openid import *
from openid import *
import os
from dns import resolver
from mango import Mango
app = Flask(__name__)
api = Api(app)
@@ -13,17 +16,31 @@ parser.add_argument('name')
parser.add_argument('value')
parser.add_argument('type')
dnsserver = '192.168.66.113'
dnsserverport = 5053
# verander de variabelen hieronder als je de script handmatig uitvoert
dnsserver = os.environ.get('DNS_SERVER')
mongo_connect = os.environ.get('MONGO_CONNECTIONSTRING')
dnsserverport = int(os.environ.get('DNS_PORT', default=53))
debug = bool(os.environ.get('API_DEBUG', default=False))
# end change
if dnsserver is None or mongo_connect is None:
print('You did not set DNS_SERVER or MONGO_CONNECTIONSTRING environ')
exit(1)
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 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
except ValueError: # geen geldige ip adres formaat
return False
return True # adres correct
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
@@ -31,30 +48,33 @@ def make_fqdn_check(subname, parentdomain):
return False
def validate_authorization(req):
jwt = req.get('Authorization')
if jwt is None:
def validate_authorization(req): # validate authorization header
jwt = req.get('Authorization') # get Authorization header
if jwt is None: # if not set
return False
jwt = jwt.split(' ')[1]
if GoogleOID.check_jwt(jwt)['error'] is False:
return True
form, value = jwt.split(' ') # get header type and value
if form == "Bearer": # if bearer(openid)
if GoogleOID.check_jwt(value)['error'] is False: # check if jwt is valid
return True
elif form == "Basic": # basic auth stored in db
if mango.check_api_key(value): # check if apikey exists in db
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}
return {'domains': ['dns.mashallah.nl'], 'error': False}
class domain(Resource):
@@ -64,7 +84,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()
@@ -77,21 +97,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
@@ -104,12 +124,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
@@ -123,7 +142,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()
@@ -136,10 +155,7 @@ class domain(Resource):
if t != 'A': # als record type niet A is
return {"error": True, "reason": "Only A records are supported"}, 400
try:
if type(ip_address(v)) is not IPv4Address: # als ip adress ongelig is
raise ValueError # raise value error
except ValueError: # geen geldige ip adres formaat
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
@@ -174,5 +190,10 @@ api.add_resource(domains, '/api/v1/dns/domains')
api.add_resource(domain, '/api/v1/dns/domain/<dmn>')
if __name__ == '__main__':
GoogleOID = GoogleOID()
app.run(debug=True, host='0.0.0.0', port=5001)
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
# do not setup dns zone globally because it errors on simultaneous requests
GoogleOID = GoogleOID() # setup google oid
mango = Mango(mongo_connect)
app.run(debug=debug, host='0.0.0.0', port=5001) # run werkzeug

View File

@@ -3,22 +3,23 @@ import datetime
class Mango:
def __init__(self, username, password, host, port):
def __init__(self, connect):
try:
self.client = MongoClient(username=username, password=password, host=host, port=port)
self.client = MongoClient(connect)
self.users = self.client['dns']['users']
self.keys = self.client['dns']['keys']
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 +31,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 +40,28 @@ 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']
def check_api_key(self, key): # True or False if api key exists in api keys db
if self.keys.find_one({"key": key}):
return True
return False

View File

@@ -2,55 +2,55 @@ class GoogleOID:
def __init__(self):
import requests
import jwt
from openid_secrets import client_secret
import os
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)
self.jwt = jwt
self.requests = requests
self.settings = {'client_id': '954325872153-1v466clrtgg6h4ptt2ne5pgpb9mhilr5.apps.googleusercontent.com',
'client_secret': client_secret,
'callback_uri': 'http://127.0.0.1.nip.io:5000/login/gcp/callback',
'key_server': 'https://www.googleapis.com/oauth2/v3/certs'}
'callback_uri': 'https://dnsgui.mashallah.nl/login/gcp/callback',
'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}

View File

@@ -0,0 +1,8 @@
flask
flask_restful
pyjwt
pyjwt[crypto]
dnspython
pymongo
werkzeug == 2.0.3 # er zit een fout in de laatste versie die plain http post requests altijd als json interperteerd

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,10 @@
FROM alpine:latest
RUN apk add --update py3-pip python3-dev gcc libc-dev libffi-dev
RUN pip install --upgrade pip
COPY requirements.txt /app/
RUN pip install --no-cache-dir -r /app/requirements.txt
COPY templates/ /app/templates/
COPY gui.py /app/
COPY mango.py /app/
COPY openid.py /app/
CMD ["python3", "/app/gui.py"]

View File

@@ -0,0 +1,41 @@
# 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

@@ -1,24 +1,36 @@
from flask import Flask, render_template, redirect, request, url_for, session
from flask import Flask, render_template, redirect, request, url_for, session, flash
import secrets
from eindopdracht.mango import Mango
import requests
from eindopdracht.openid import *
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 = 'dns.mashallah.nl'
dns_api = os.environ.get('DNS_API')
mongo_connection = os.environ.get('MONGO_CONNECTIONSTRING')
debug = bool(os.environ.get('API_DEBUG', default=False)) # debug option as bool
# end editing
if dns_api is None or mongo_connection is None:
# check if vars are not set
print('Missing DNS or MONGO environs')
exit(1)
app = Flask(__name__)
app.secret_key = 'fdsfdsafadfa'
dns_api = 'http://localhost:5001'
zone = 'school.test'
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}
@@ -27,9 +39,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):
@@ -37,18 +50,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'
@@ -59,92 +74,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:
return 'Account is unavailable for login'
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 na is None or ty is None or va is None or rq is None:
return 'Missing form data, you did not use the dashboard!'
if rq != "Add" and rq != "Delete" and rq != "Update":
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':
response = record_update(name=na, typ=ty, value=va)
if rq == 'Add' or rq == 'Update': # if user adds or updates a record
jwt = db.google_get_jwt(session['username']) # get jwt from db
if response['error'] is True:
response = record_update(name=na, typ=ty, value=va, jwt=jwt) # update record
if response['error'] is True: # if error with update record
return f"Error processing request: {response['reason']}"
elif rq == 'Delete':
response = record_delete(name=na)
jwt = db.google_get_jwt(session['username']) # get jwt from db
if response['error'] is True:
response = record_delete(name=na, jwt=jwt) # delete record
if response['error'] is True: # if error with delete record
return f"Error processing request: {response['reason']}"
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)
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": # user adds a sub
db.google_add_new_sub(va) # add sub to db
# 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('root', 'test', '192.168.66.113', 5027)
GoogleOID = GoogleOID()
app.run(debug=True, host='0.0.0.0')
db = Mango(mongo_connection) # setup mango
GoogleOID = GoogleOID() # initialize googleoid
app.run(debug=debug, host='0.0.0.0') # run werkzeug

View File

@@ -0,0 +1,67 @@
from pymongo import MongoClient
import datetime
class Mango:
def __init__(self, connect):
try:
self.client = MongoClient(connect)
self.users = self.client['dns']['users']
self.keys = self.client['dns']['keys']
except ConnectionError:
print('MongoDB connection error')
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: # None if nothing found
return {"error": True, "reason": "User not found"}
else:
return {"error": False}
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:
return {"error": True, "reason": "User not found"}
found['sso']['google']['lastlogin'] = datetime.datetime.utcnow()
self.users.replace_one({"sso.google.profile.sub": uuid}, found)
return {"error": False}
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
self.users.replace_one({"sso.google.profile.sub": profile['sub']}, found)
return {"error": False}
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): # 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): # 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): # 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): # return jwt as stored in db for given user
found = self.users.find_one({"sso.google.profile.sub": uuid})
return found['sso']['google']['jwt']
def check_api_key(self, key): # True or False if api key exists in api keys db
if self.keys.find_one({"key": key}):
return True
return False

View File

@@ -0,0 +1,56 @@
class GoogleOID:
def __init__(self):
import requests
import jwt
import os
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)
self.jwt = jwt
self.requests = requests
self.settings = {'client_id': '954325872153-1v466clrtgg6h4ptt2ne5pgpb9mhilr5.apps.googleusercontent.com',
'client_secret': client_secret,
'callback_uri': 'https://dnsgui.mashallah.nl/login/gcp/callback',
'key_server': 'https://www.googleapis.com/oauth2/v3/certs'} # global oid settings
def settings(self): # make it so that the settings variable is callable
return self.settings
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) # exchange code for token
if r.status_code != 200: # if not successful
return {'error': True, 'reason': f'Could not exchange code for access key {r.status_code}'}
return {'error': False, 'data': r.json()}
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 not successful
return {'error': True, 'reason': f'Could not get profile info {r.status_code}'}
return {'error': False, 'profile': r.json()}
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: # 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: # catch generic error
return {'error': True, 'reason': 'Error decoding JWT'}
return {'error': False, 'data': decoded}

View File

@@ -0,0 +1,6 @@
flask
pyjwt
requests
pymongo
werkzeug == 2.0.3 # er zit een fout in de laatste versie die plain http post requests altijd als json interperteerd

View File

@@ -37,6 +37,32 @@ th {
<td>{{ profile['sub'] }}</td>
<td>{{ lastlogin }}</td>
</tr>
<tr>
<td>
<form method="POST">
<input type="submit" value="Query JWT" name="request" >
</form>
</td>
{% with messages = get_flashed_messages() %}
{% if messages %}
{% for message in messages %}
<td>
<input type="text" value="{{ message }}">
</td>
{% endfor %}
{% endif %}
{% endwith %}
</tr>
<form method="POST">
<tr>
<td>
<input type="submit" value="Add SUB" name="request" >
</td>
<td>
<input placeholder="Google UUID" name="value">
</td>
</tr>
</form>
</table>
<hr>
<h4>DNS Records school.test</h4>

View File

@@ -11,5 +11,7 @@
<input type="text" placeholder="Username"><br>
<input type="password" placeholder="password"><br>
<a href="login/gcp/start"><button>Login with Google you scrub</button></a>
<br>
<p>Warning! By logging in you agree to submit your userdata such as full name and email address to the Republic of India</p>
</body>
</html>

View File

@@ -0,0 +1,40 @@
import json
import boto3
import base64
def lambda_handler(event, context):
method = str(event.get('requestContext').get('http').get('method'))
dynamodb = boto3.resource('dynamodb')
table = dynamodb.Table('lambdatable')
if method == "POST":
key = str(event.get('pathParameters').get('id'))
url = str(base64.b64decode(event.get('body')), 'utf-8')
table.put_item(Item={'key': key, 'url': url})
return {'message': url}
elif method == "DELETE":
key = str(event.get('pathParameters').get('id'))
table.delete_item(Key={'key': key})
return {'message': key}
else:
key = str(event.get('pathParameters').get('id'))
data = table.get_item(Key={"key": key})
if len(data) is 1:
return {'message': 'No such key'}
response = {
"statusCode": 302,
"headers": {
'Location': data['Item']['url']
}
}
return response

View File

@@ -0,0 +1,14 @@
$ORIGIN nl.
$TTL 1m
dns.mashallah IN SOA dnsns.mashallah.nl. admin.dns.mashallah.nl. (
0 ; serial
4h ; refresh (zone slave must request new zone file)
15m ; retry (zone slave pulls after a failed attempt)
8h ; expire (zone slave abstains further responses without zone refresh)
5m) ; Negative caching TTL (TTL of SOA record)
IN NS dnsns.mashallah.nl.
$ORIGIN dns.mashallah.nl.
test IN A 192.168.0.1

View File

@@ -2,9 +2,9 @@
// Do any local configuration here
//
zone "school.test" in{
zone "dns.mashallah.nl" in{
type master;
file "school.test.zone";
file "/var/lib/bind/dns.mashallah.nl.zone";
allow-update{any;};
};

View File

@@ -1,15 +0,0 @@
$ORIGIN test.
$TTL 5m
school IN SOA ns1.school.test. admin.school.test. (
0 ; serial
4h ; refresh
15m ; retry
8h ; expire
4m) ; Negative caching TTL
IN NS ns1.school.test.
IN NS ns2.school.test.
$ORIGIN school.test.
ns1 IN A 10.0.1.1
ns2 IN A 10.0.2.1

View File

@@ -4,7 +4,7 @@ services:
image: ubuntu/bind9
volumes:
- ./bind/named.conf.local:/etc/bind/named.conf.local:ro
- ./bind/school.test.zone:/var/lib/bind/school.test.zone:ro
- ./bind/dns.mashallah.nl.zone:/var/lib/bind/dns.mashallah.nl.zone:ro
ports:
- "5053:53/tcp"
- "5053:53/udp"
@@ -17,6 +17,7 @@ services:
volumes:
- ./mongo/import.sh:/docker-entrypoint-initdb.d/import.sh:ro
- ./mongo/test-data.json:/docker-entrypoint-initdb.d/test-data.json:ro
- ./mongo/test-keys.json:/docker-entrypoint-initdb.d/test-keys.json:ro
ports:
- "5027:27017"
@@ -30,4 +31,23 @@ services:
ME_CONFIG_MONGODB_ADMINPASSWORD: test
ME_CONFIG_MONGODB_URL: mongodb://root:test@mongo:27017/
depends_on:
- mongo
- mongo
api:
image: 4grxfq/api
ports:
- "5001:5001"
environment:
DNS_SERVER: bind
DNS_PORT: 53
OPENID_SECRET: CHANGEME
MONGO_CONNECTIONSTRING: "mongodb://root:test@mongo:27017"
gui:
image: 4grxfq/gui
ports:
- "5000:5000"
environment:
OPENID_SECRET: CHANGEME
DNS_API: "http://api:5001"
MONGO_CONNECTIONSTRING: "mongodb://root:test@mongo:27017"

View File

@@ -1,2 +1,3 @@
#!/bin/sh
mongoimport /docker-entrypoint-initdb.d/test-data.json -d dns -c users --drop -u root -p test --authenticationDatabase admin
mongoimport /docker-entrypoint-initdb.d/test-data.json -d dns -c users --drop -u root -p test --authenticationDatabase admin
mongoimport /docker-entrypoint-initdb.d/test-keys.json -d dns -c keys --drop -u root -p test --authenticationDatabase admin

View File

@@ -0,0 +1,3 @@
{
"key": "qQT0IuiJwTIz5Jlxw7CwFEeNdcPJUzQqM16PVebJUqaXcLsNFiSVgr8se74itZA="
}

View File

@@ -1,50 +1,55 @@
1. regristreer gcp een applicatie en genereer client keys
2. zet op login met google button
2. zet een webserver functie op die de client redirect naar google met de onderstaande GET parameters
3. de button opent functie dat de volgende request stuurt naar de url
3. plaats een knop of hyperlink op de home pagina die naar de bovenste functie redirect
4. de flask applicatie redirect de client naar de onderstaande parameter, de onderstaande GET request wordt dus door de
client uitgevoerd
```
GET https://accounts.google.com/o/oauth2/v2/auth?
client_id=CLIENTID &
response_type=code &
scope=openid profile email &
redirect_uri=CALLBACK &
nonce=RANDOM &
client_id=CLIENTID & # de client id van je applicatie die je bij stap 1 hebt gegenereerd
response_type=code & # je vraagt google om een code(deze kan je met je app secret van stap 1 een authorization token verkrijgen)
scope=openid profile email & # de data die je opvraagt(openid=jwt profile=naam, foto enz... email=email)
redirect_uri=CALLBACK & # waar google de client naar redirect met parameters
nonce=RANDOM # om een replay attack te voorkomen
RESPONSE
GET HTTP REDIRECT CALLBACK # een get request naar de callback met de volgende arguments
code=AUTHORIZATIONCODE &
scope=email profile
authuser=0
prompt=none
RESPONSE # de onderstaande krijgt je applicatie terug op je callback
GET http://localhost:5000/callback # een get request naar de callback met de volgende arguments
code=AUTHORIZATIONCODE & # de code die je moet uitwisselen met je secret key
scope=email profile & # de scopes die je mag opvragen
authuser=0 & #
prompt=none #
```
Hiervan moeten we de code parameter verkrijgen
Hiervan moeten we de code parameter gebruiken
4. Nadat je de authorization code hebt verkregen moet je die omzetten in een (refresh)token, hierbij krijg je ook een
5. Nadat je de authorization code hebt verkregen moet je die omzetten in een (refresh)token, hierbij krijg je ook een
jwt met alle gebruiker profiel data.
6. De onderstaande request moet je applicatie in de achtergrond uitvoeren om de code om te wisselen naar bruikbare data
```
POST https://oauth2.googleapis.com/token?
code=AUTORIZATIONCODE &
client_id=CLIENTID
client_secret=CSECRET &
redirect_uri=CALLBACK & # wordt niet gebruikt wel verplicht
grant_type=authorization_code
code=AUTORIZATIONCODE & # de code die je van de client hebt gekregen
client_id=CLIENTID & # je applicatie id
client_secret=CSECRET & # je applicatie secret
redirect_uri=CALLBACK & # wordt niet gebruikt wel verplicht
grant_type=authorization_code # de type code die je meegeeft
RESPONSE
200 OK
{
"access_token": "ACCESS_TOKEN", # hoeft in principe niets mee gedaan te worden
"expires_in": 3312,
"scope": "https://www.googleapis.com/auth/userinfo.profile openid https://www.googleapis.com/auth/userinfo.email",
"token_type": "Bearer",
"id_token": "aaaa.bbbbbbbbbbbbbbbb.cccccccccc"
# de JWT, als je deze checkt met de keys van google is de authorisatie voldoende, in het midden van de 2 punten is de profiel informatie te vinden encoded in base64
"access_token": "ACCESS_TOKEN", # deze kan je gebruiken om extra profiel data op te vragen bij de google profile api
"expires_in": 3312, # de tijd voor hoelang de access_token geldig is in seconden
"scope": "https://www.googleapis.com/auth/userinfo.profile openid https://www.googleapis.com/auth/userinfo.email", # de scopes die je access_token mag benaderen
"token_type": "Bearer", # de onderstaande id_token type
"id_token": "aaaa.bbbbbbbbbbbbbbbb.cccccccccc" # de JWT, als je deze checkt met de keys van google is de authorisatie voldoende, in het midden van de 2 punten is de profiel informatie te vinden encoded in base64
}
```
5. Nadat je de response hebt gekeken moet je de id_token maniluperen zodat je de base64 encoded object tussen de twee
punten verkrijgt.
7. in principe voor puur openid authenticatie moet je de client de jwt toesturen, hiermee authentiseerd de client dan
met je applicatie zolang de jwt geldig is.
6. In de base64 encoded JWT staat alle profiel data, bekijk de database of de sub key overeenkomt met wat er is opgeslagen
8. aan de flask kant moet je de Authorization header van elke request van de client controleren of de meegegeven JWT
Bearer token nog geldig is, door google is uitgegeven, geldend voor jouw applicatie, hier zijn libraries voor te vinden.

View File

@@ -0,0 +1,10 @@
#!/bin/sh
git pull
cd dns_api
docker build . -t 4grxfq/api:latest
cd ..
cd dns_gui
docker build . -t 4grxfq/gui:latest
cd ..
docker push 4grxfq/api:latest
docker push 4grxfq/gui:latest