Change to oidc and new foreign id format
All checks were successful
ci/woodpecker/manual/woodpecker Pipeline was successful
All checks were successful
ci/woodpecker/manual/woodpecker Pipeline was successful
This commit is contained in:
parent
d0fa0b1928
commit
a28189a306
18
.dockerignore
Normal file
18
.dockerignore
Normal file
@ -0,0 +1,18 @@
|
||||
.kpt-pipeline/
|
||||
.git/
|
||||
.gitignore
|
||||
deployment.yaml
|
||||
LICENSE
|
||||
README.md
|
||||
skaffold.yaml
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
**/*.suo
|
||||
**/*.ntvs*
|
||||
**/*.njsproj
|
||||
**/*.sln
|
||||
**/*.sw?
|
||||
**/*.kpt-pipeline
|
114
deployment.yaml
Normal file
114
deployment.yaml
Normal file
@ -0,0 +1,114 @@
|
||||
---
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: inventory-app
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: inventory-app
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: inventory-app
|
||||
spec:
|
||||
enableServiceLinks: false
|
||||
imagePullSecrets:
|
||||
- name: regcred
|
||||
serviceAccountName: oidc-gateway
|
||||
containers:
|
||||
- name: inventory-app
|
||||
image: inventory-app
|
||||
env:
|
||||
- name: PYTHONUNBUFFERED
|
||||
value: "1"
|
||||
- name: RECAPTCHA_PUBLIC_KEY
|
||||
value: 6LeIxAcTAAAAAJcZVRqyHh71UMIEGNQ_MXjiZKhI
|
||||
- name: RECAPTCHA_PRIVATE_KEY
|
||||
value: 6LeIxAcTAAAAAGG-vFI1TnRWxMZNFuojJ4WifJWe
|
||||
- name: MEMBERS_HOST
|
||||
value: "https://members.k-space.ee"
|
||||
- name: INVENTORY_ASSETS_BASE_URL
|
||||
value: "https://minio.codemowers.eu:9000"
|
||||
- name: MONGO_URI
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: mongodb-application-readwrite
|
||||
key: connectionString.standard
|
||||
- name: AWS_ENDPOINT_URL
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: miniobucket-inventory-app-owner-secrets
|
||||
key: MINIO_URI
|
||||
- name: SECRET_KEY
|
||||
value: "bad_secret"
|
||||
- name: ENVIRONMENT_TYPE
|
||||
value: "DEV"
|
||||
- name: MY_POD_NAME
|
||||
valueFrom:
|
||||
fieldRef:
|
||||
fieldPath: spec.nodeName
|
||||
envFrom:
|
||||
- secretRef:
|
||||
name: oidc-client-inventory-app-owner-secrets
|
||||
ports:
|
||||
- containerPort: 5000
|
||||
name: metrics
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: inventory-app
|
||||
labels:
|
||||
app: inventory-app
|
||||
spec:
|
||||
selector:
|
||||
app: inventory-app
|
||||
ports:
|
||||
- protocol: TCP
|
||||
port: 5000
|
||||
---
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: inventory-app
|
||||
annotations:
|
||||
kubernetes.io/ingress.class: shared
|
||||
traefik.ingress.kubernetes.io/router.entrypoints: websecure
|
||||
traefik.ingress.kubernetes.io/router.tls: "true"
|
||||
external-dns.alpha.kubernetes.io/target: traefik.codemowers.ee
|
||||
spec:
|
||||
rules:
|
||||
- host: inventory-app-72zn4.codemowers.ee
|
||||
http:
|
||||
paths:
|
||||
- pathType: Prefix
|
||||
path: "/"
|
||||
backend:
|
||||
service:
|
||||
name: inventory-app
|
||||
port:
|
||||
number: 5000
|
||||
tls:
|
||||
- hosts:
|
||||
- "*.codemowers.ee"
|
||||
---
|
||||
apiVersion: codemowers.io/v1alpha1
|
||||
kind: OIDCGWClient
|
||||
metadata:
|
||||
name: inventory-app
|
||||
spec:
|
||||
uri: 'https://inventory-app-72zn4.codemowers.ee'
|
||||
redirectUris:
|
||||
- 'https://inventory-app-72zn4.codemowers.ee/login-callback'
|
||||
grantTypes:
|
||||
- 'authorization_code'
|
||||
responseTypes:
|
||||
- 'code'
|
||||
availableScopes:
|
||||
- 'openid'
|
||||
- 'profile'
|
||||
tokenEndpointAuthMethod: 'client_secret_basic'
|
||||
pkce: false
|
||||
|
49
inventory-app/api.py
Normal file
49
inventory-app/api.py
Normal file
@ -0,0 +1,49 @@
|
||||
import const
|
||||
from pymongo import MongoClient
|
||||
from flask import Blueprint, abort, g, make_response, redirect, render_template, request, jsonify
|
||||
from common import CustomForm, build_query, flatten, format_name, spam
|
||||
from kubernetes import client, config
|
||||
|
||||
page_api = Blueprint("api", __name__)
|
||||
db = MongoClient(const.MONGO_URI).get_default_database()
|
||||
|
||||
def get_users():
|
||||
config.load_incluster_config()
|
||||
api_instance = client.CustomObjectsApi()
|
||||
ret = api_instance.list_namespaced_custom_object("codemowers.io", "v1alpha1", "default", "oidcgatewayusers")
|
||||
resp = []
|
||||
for item in ret["items"]:
|
||||
resp.append(item)
|
||||
return resp
|
||||
|
||||
@page_api.route("/users")
|
||||
def view_users():
|
||||
resp = get_users()
|
||||
print(resp)
|
||||
return jsonify(resp)
|
||||
|
||||
@page_api.route("/cards")
|
||||
def get_group_cards():
|
||||
group = request.args.get("group", False)
|
||||
if not group:
|
||||
return "must specify group in parameter", 400
|
||||
print(group)
|
||||
gu = list(filter(lambda u: any(g["name"] == group for g in u["status"]["groups"]), get_users()))
|
||||
keys = list(map(lambda u: u["metadata"]["name"], gu))
|
||||
print(keys)
|
||||
flt = {
|
||||
"token.uid_hash": {"$exists": True},
|
||||
"inventory.owner.foreign_id": {"$in": keys}
|
||||
}
|
||||
prj = {
|
||||
"inventory.owner": True,
|
||||
"token.uid_hash": True
|
||||
}
|
||||
found = []
|
||||
for obj in db.inventory.find(flt, prj):
|
||||
del obj["_id"]
|
||||
if obj["inventory"] and obj["inventory"]["owner"] and type(obj["inventory"]["owner"]["foreign_id"]) != str:
|
||||
del obj["inventory"]
|
||||
found.append(obj)
|
||||
return jsonify(list(found))
|
||||
|
@ -26,13 +26,14 @@ def inventory_fetch(owner=True, item_type=None):
|
||||
def wrapper(f):
|
||||
@wraps(f)
|
||||
def decorated_function(*args, **kwargs):
|
||||
user = read_user()
|
||||
d = {
|
||||
"_id": ObjectId(kwargs.pop("item_id"))
|
||||
}
|
||||
if item_type:
|
||||
d["type"] = item_type
|
||||
if owner:
|
||||
d["inventory.owner.foreign_id"] = g.user["_id"]
|
||||
d["inventory.owner.foreign_id"] = user["username"]
|
||||
kwargs["item"] = db.inventory.find_one(d)
|
||||
return f(*args, **kwargs)
|
||||
return decorated_function
|
||||
|
@ -1,105 +0,0 @@
|
||||
import secrets
|
||||
import string
|
||||
from datetime import datetime
|
||||
from functools import wraps
|
||||
|
||||
from bson.objectid import ObjectId
|
||||
from flask import abort, g, make_response, redirect, request, session
|
||||
from pymongo import MongoClient
|
||||
|
||||
import const
|
||||
|
||||
db = MongoClient(const.MONGO_URI).get_default_database()
|
||||
|
||||
|
||||
def generate_password(length):
|
||||
letters = string.ascii_letters + string.digits + string.punctuation
|
||||
pw = secrets.choice(string.ascii_lowercase)
|
||||
pw += secrets.choice(string.ascii_uppercase)
|
||||
pw += secrets.choice(string.digits)
|
||||
pw += secrets.choice(string.punctuation)
|
||||
pw += "".join(secrets.choice(letters) for i in range(length - 4))
|
||||
return "".join(secrets.SystemRandom().sample(pw, length))
|
||||
|
||||
def check_login():
|
||||
token = db.token.find_one({"cookie": request.cookies.get("LOGINLINKCOOKIE", "")})
|
||||
if not token:
|
||||
cookie = generate_password(50)
|
||||
res = db.token.update_one({
|
||||
"token": request.args.get("token"),
|
||||
"cookie": {"$exists": False}
|
||||
}, {
|
||||
"$set": {
|
||||
"used": datetime.utcnow(),
|
||||
"cookie": cookie
|
||||
}
|
||||
})
|
||||
if res.matched_count >= 1:
|
||||
return (1, cookie)
|
||||
else:
|
||||
return (2, cookie)
|
||||
|
||||
db.token.update_one({
|
||||
"_id": token["_id"]
|
||||
}, {
|
||||
"$set": {
|
||||
"last_seen": datetime.utcnow(),
|
||||
"user_agent": request.headers.get("User-Agent"),
|
||||
"remote_addr": request.remote_addr
|
||||
}
|
||||
})
|
||||
|
||||
g.user = db.member.find_one({"_id": ObjectId(token["member_id"])})
|
||||
g.token = token
|
||||
return (0, None);
|
||||
|
||||
def has_login():
|
||||
r, _ = check_login()
|
||||
return r == 0
|
||||
|
||||
def login_redirect():
|
||||
session['target_path'] = request.path
|
||||
return redirect("/login")
|
||||
|
||||
def login_required(f):
|
||||
@wraps(f)
|
||||
def decorated_function(*args, **kwargs):
|
||||
r, cookie = check_login()
|
||||
if r == 1:
|
||||
resp = make_response()
|
||||
resp.set_cookie("LOGINLINKCOOKIE", cookie)
|
||||
resp.headers["location"] = request.path
|
||||
return resp, 302
|
||||
elif r == 2:
|
||||
return login_redirect()
|
||||
return f(*args, **kwargs)
|
||||
return decorated_function
|
||||
|
||||
|
||||
def rget(d, attr):
|
||||
if not d:
|
||||
return None
|
||||
if "." in attr:
|
||||
pre, post = attr.split(".", 1)
|
||||
return rget(d.get(pre), post)
|
||||
else:
|
||||
return d.get(attr)
|
||||
|
||||
|
||||
def required(path, msg=None, status=403):
|
||||
def ff(f):
|
||||
@wraps(f)
|
||||
def decorated_function(*args, **kwargs):
|
||||
if not rget(g.user, path):
|
||||
abort(status, description=msg)
|
||||
return f(*args, **kwargs)
|
||||
return decorated_function
|
||||
return ff
|
||||
|
||||
|
||||
def board_member_required(f):
|
||||
@wraps(f)
|
||||
def decorated_function(*args, **kwargs):
|
||||
_f = required("access.board", "K-SPACE MTÜ board member required")
|
||||
return _f(f)(*args, **kwargs)
|
||||
return decorated_function
|
@ -12,7 +12,7 @@ from wtforms.validators import Length
|
||||
|
||||
import const
|
||||
from common import CustomForm, build_query, flatten, format_name, spam
|
||||
from decorators import has_login, login_redirect, login_required
|
||||
from oidc import page_oidc, login_required, read_user
|
||||
|
||||
page_inventory = Blueprint("inventory", __name__)
|
||||
db = MongoClient(const.MONGO_URI).get_default_database()
|
||||
@ -21,9 +21,9 @@ db = MongoClient(const.MONGO_URI).get_default_database()
|
||||
def view_inventory_view(item_id):
|
||||
template = "inventory_view.html"
|
||||
item = db.inventory.find_one({ "_id": ObjectId(item_id) })
|
||||
if not has_login():
|
||||
if not read_user():
|
||||
if not item["inventory"].get("public"):
|
||||
return login_redirect()
|
||||
return do_login()
|
||||
template = "inventory_view_public.html"
|
||||
base_url = const.INVENTORY_ASSETS_BASE_URL
|
||||
photo_url = "%s/kspace-inventory/%s" % (base_url, item_id)
|
||||
@ -355,7 +355,7 @@ def view_inventory(slug=None):
|
||||
q = {"type": {"$ne": "token"}}
|
||||
template = "inventory.html"
|
||||
public_view = False
|
||||
if not has_login():
|
||||
if not read_user():
|
||||
q.update({"inventory.public": True})
|
||||
template = "inventory_public.html"
|
||||
public_view = True
|
||||
@ -409,28 +409,29 @@ def view_inventory(slug=None):
|
||||
{ "$match": q2 },
|
||||
{ "$sort": { sort_field_final : 1 if sort_direction == "asc" else -1 } }
|
||||
])
|
||||
|
||||
return render_template(template, **locals())
|
||||
|
||||
|
||||
@page_inventory.route("/m/inventory/<item_id>/claim", methods=["POST"])
|
||||
@login_required
|
||||
def view_inventory_claim(item_id):
|
||||
user = read_user()
|
||||
db.inventory.update_one({
|
||||
"_id": ObjectId(item_id),
|
||||
"inventory.owner.foreign_id": None
|
||||
}, {
|
||||
"$set": {
|
||||
"inventory.owner.foreign_id": ObjectId(g.user["_id"]),
|
||||
"inventory.owner.display_name": g.user["full_name"],
|
||||
"inventory.owner.foreign_id": user["username"],
|
||||
"inventory.owner.display_name": user["name"],
|
||||
},
|
||||
})
|
||||
return redirect("/m/user/%s" % g.user["_id"])
|
||||
return redirect("/m/inventory/%s/view" % item_id)
|
||||
|
||||
|
||||
@page_inventory.route("/m/inventory/<item_id>/use", methods=["POST"])
|
||||
@login_required
|
||||
def view_inventory_use(item_id):
|
||||
user = read_user()
|
||||
item = db.inventory.find_one({
|
||||
"_id": ObjectId(item_id),
|
||||
"inventory.usable": True,
|
||||
@ -443,25 +444,26 @@ def view_inventory_use(item_id):
|
||||
"_id": ObjectId(item["_id"])
|
||||
}, {
|
||||
"$set": {
|
||||
"inventory.user.foreign_id": ObjectId(g.user["_id"]),
|
||||
"inventory.user.display_name": g.user["full_name"],
|
||||
"inventory.user.foreign_id": ObjectId(user["username"]),
|
||||
"inventory.user.display_name": user["name"],
|
||||
},
|
||||
})
|
||||
name = format_name(item)
|
||||
msg = "%s has started using %s" % (g.user["full_name"], name)
|
||||
item_name = format_name(item)
|
||||
msg = "%s has started using %s" % (user["name"], item_name)
|
||||
if item.get("shortener") and item["shortener"].get("slug"):
|
||||
msg += ("\nk6.ee/%s" % item["shortener"]["slug"])
|
||||
spam(msg)
|
||||
return redirect("/m/user/%s" % g.user["_id"])
|
||||
return redirect("/m/inventory/%s/view" % item_id)
|
||||
|
||||
|
||||
@page_inventory.route("/m/inventory/<item_id>/vacate", methods=["POST"])
|
||||
@login_required
|
||||
def view_inventory_vacate(item_id):
|
||||
user = read_user()
|
||||
item = db.inventory.find_one({
|
||||
"_id": ObjectId(item_id),
|
||||
"inventory.usable": True,
|
||||
"inventory.user.foreign_id": ObjectId(g.user["_id"])
|
||||
"inventory.user.foreign_id": ObjectId(user["username"])
|
||||
})
|
||||
if not item:
|
||||
return abort(404)
|
||||
@ -473,9 +475,9 @@ def view_inventory_vacate(item_id):
|
||||
"inventory.user": ""
|
||||
},
|
||||
})
|
||||
name = format_name(item)
|
||||
msg = "%s has stopped using %s" % (g.user["full_name"], name)
|
||||
item_name = format_name(item)
|
||||
msg = "%s has stopped using %s" % (user["name"], item_name)
|
||||
if item.get("shortener") and item["shortener"].get("slug"):
|
||||
msg += ("\nk6.ee/%s" % item["shortener"]["slug"])
|
||||
spam(msg)
|
||||
return redirect("/m/user/%s" % g.user["_id"])
|
||||
return redirect("/m/inventory/%s/view" % item_id)
|
||||
|
@ -46,8 +46,19 @@ from wtforms.validators import DataRequired
|
||||
|
||||
import const
|
||||
from common import CustomForm, devenv, flatten, format_name, spam
|
||||
from decorators import board_member_required, generate_password, login_required
|
||||
from inventory import page_inventory
|
||||
from oidc import page_oidc, login_required, read_user
|
||||
from api import page_api
|
||||
|
||||
def check_foreign_key_format(item):
|
||||
try:
|
||||
if type(item["inventory"]["owner"]["foreign_id"]) == ObjectId:
|
||||
return True
|
||||
if type(item["inventory"]["user"]["foreign_id"]) == ObjectId:
|
||||
return True
|
||||
except:
|
||||
pass
|
||||
return False
|
||||
|
||||
def render_markdown(text):
|
||||
if not text:
|
||||
@ -94,6 +105,7 @@ jinja2.filters.FILTERS['owner_link'] = render_owner_link
|
||||
jinja2.filters.FILTERS['user_link'] = render_user_link
|
||||
jinja2.filters.FILTERS['is_list'] = is_list
|
||||
jinja2.filters.FILTERS['quote_plus'] = lambda u: urllib.parse.quote_plus(u)
|
||||
jinja2.filters.FILTERS['check_foreign_key_format'] = check_foreign_key_format
|
||||
env = Environment(loader=FileSystemLoader('templates/'))
|
||||
|
||||
|
||||
@ -110,6 +122,8 @@ class ReverseProxied(object):
|
||||
app = Flask(__name__)
|
||||
app.wsgi_app = ReverseProxied(app.wsgi_app)
|
||||
app.register_blueprint(page_inventory)
|
||||
app.register_blueprint(page_oidc)
|
||||
app.register_blueprint(page_api)
|
||||
metrics = PrometheusMetrics(app, group_by="path")
|
||||
|
||||
app.config['SECRET_KEY'] = const.SECRET_KEY
|
||||
@ -172,82 +186,26 @@ def name_check(form, field):
|
||||
if any(c.isdigit() for c in field.data):
|
||||
raise ValidationError("Name must not contain numbers")
|
||||
|
||||
@dev_only
|
||||
@app.route("/dev_login")
|
||||
@app.route("/dev_login/<full_name>")
|
||||
def dev_login(full_name=None):
|
||||
allowed_users = ["Mickey Mouse", "Donald Duck"]
|
||||
if not full_name or full_name not in allowed_users:
|
||||
full_name = "Mickey Mouse"
|
||||
member = mongodb.member.find_one({"full_name": full_name})
|
||||
|
||||
token = generate_password(20)
|
||||
d = {
|
||||
"member_id": member["_id"],
|
||||
"full_name": member["full_name"],
|
||||
"token": token,
|
||||
"method": "link"
|
||||
}
|
||||
mongodb.token.insert_one(d)
|
||||
url = "%sm/profile?token=%s" % (request.url_root, urllib.parse.quote_plus(token))
|
||||
return redirect(url, code=302)
|
||||
|
||||
@app.route("/m/user/<member_id>/login")
|
||||
@metrics.do_not_track()
|
||||
@login_required
|
||||
def view_send_login_link(member_id):
|
||||
user = g.user
|
||||
member = mongodb.member.find_one({"_id": ObjectId(member_id), "enabled": True})
|
||||
if not member:
|
||||
raise
|
||||
send_login_link(member,
|
||||
provider = g.user["full_name"])
|
||||
return redirect("/m/user/%s" % member_id)
|
||||
|
||||
|
||||
class LoginLinkForm(CustomForm):
|
||||
email = EmailField('Email address', validators = [validators.DataRequired(), validators.Email()])
|
||||
recaptcha = RecaptchaField()
|
||||
|
||||
@app.route("/login/address", methods=["POST"])
|
||||
def request_login_link():
|
||||
form = LoginLinkForm(request.form)
|
||||
if not form.validate_on_submit():
|
||||
return abort(400)
|
||||
sleep(secrets.SystemRandom().randint(10, 40))
|
||||
member = mongodb.member.find_one({"mail": form.email.data, "enabled": True})
|
||||
if member:
|
||||
send_login_link(member)
|
||||
return render_template("login_link_request.html", **locals())
|
||||
|
||||
# just to manually enter login_required annotation
|
||||
@app.route("/login")
|
||||
def view_login():
|
||||
form = LoginLinkForm()
|
||||
return render_template("login.html", **locals())
|
||||
@login_required
|
||||
def login_dummy():
|
||||
return redirect("/m/inventory")
|
||||
|
||||
@app.route("/")
|
||||
def index():
|
||||
return redirect("/m/inventory")
|
||||
|
||||
@app.route("/login/authelia")
|
||||
def view_login_authelia():
|
||||
redirect_location = session.pop('target_path', "/m/profile")
|
||||
username = request.headers.get("Remote-User")
|
||||
if not username:
|
||||
raise ValueError("Ding dong")
|
||||
@app.route("/me")
|
||||
@login_required
|
||||
def view_profile():
|
||||
user = read_user()
|
||||
return f"Hello {user['name']}"
|
||||
|
||||
member = mongodb.member.find_one({"ad.username": username, "enabled":True})
|
||||
if not member:
|
||||
return render_template("login_error.html")
|
||||
d = {
|
||||
"member_id": member["_id"],
|
||||
"full_name": member["full_name"],
|
||||
"method": "authelia",
|
||||
"cookie": generate_password(50)
|
||||
}
|
||||
|
||||
mongodb.token.insert_one(d)
|
||||
resp = make_response()
|
||||
resp.set_cookie("LOGINLINKCOOKIE", d["cookie"])
|
||||
resp.headers['location'] = redirect_location
|
||||
return resp, 302
|
||||
@app.route("/hello")
|
||||
def view_hello():
|
||||
return "Hello!"
|
||||
|
||||
class MultiCheckboxField(SelectMultipleField):
|
||||
widget = widgets.ListWidget(prefix_label=False)
|
||||
|
100
inventory-app/oidc.py
Normal file
100
inventory-app/oidc.py
Normal file
@ -0,0 +1,100 @@
|
||||
import os
|
||||
import const
|
||||
import jwt
|
||||
import base64
|
||||
import requests
|
||||
from pymongo import MongoClient
|
||||
from flask import Blueprint, abort, g, make_response, redirect, render_template, request, Flask, request, url_for, session
|
||||
from common import CustomForm, build_query, flatten, format_name, spam
|
||||
from functools import wraps
|
||||
|
||||
page_oidc = Blueprint("oidc", __name__)
|
||||
db = MongoClient(const.MONGO_URI).get_default_database()
|
||||
metadata = requests.get("https://auth.codemowers.eu/.well-known/openid-configuration").json()
|
||||
|
||||
def login_required(f):
|
||||
@wraps(f)
|
||||
def decorated_function(*args, **kwargs):
|
||||
if not read_user():
|
||||
print("doing login redirect")
|
||||
session["original_url"] = request.full_path
|
||||
return do_login()
|
||||
return f(*args, **kwargs)
|
||||
return decorated_function
|
||||
|
||||
def do_login():
|
||||
url = add_url_params(metadata["authorization_endpoint"], {
|
||||
"client_id": os.getenv("OIDC_CLIENT_ID"),
|
||||
"redirect_uri": url_for("oidc.login_callback", _external=True, _scheme='https'),
|
||||
"response_type": "code",
|
||||
"scope": "openid profile",
|
||||
})
|
||||
return redirect(url)
|
||||
|
||||
def add_url_params(url, params):
|
||||
req = requests.models.PreparedRequest()
|
||||
req.prepare_url(url, params)
|
||||
return req.url
|
||||
|
||||
@page_oidc.route('/login-callback')
|
||||
def login_callback():
|
||||
code = request.args.get('code')
|
||||
r = requests.post(metadata["token_endpoint"], {
|
||||
"code": code,
|
||||
"grant_type": "authorization_code",
|
||||
"redirect_uri": url_for("oidc.login_callback", _external=True, _scheme='https'),
|
||||
"client_id": os.getenv("OIDC_CLIENT_ID"),
|
||||
"client_secret": os.getenv("OIDC_CLIENT_SECRET"),
|
||||
}).json()
|
||||
if "error" in r:
|
||||
return "failed to fetch tokens", 500
|
||||
if not validate_id_token(r["id_token"]) or not read_user(r["access_token"]):
|
||||
return "tokens validation failed", 500
|
||||
print("authenticated")
|
||||
session["id_token"] = r["id_token"]
|
||||
session["access_token"] = r["access_token"]
|
||||
return redirect(session.pop("original_url", "/"))
|
||||
|
||||
@page_oidc.route("/logout")
|
||||
def logout():
|
||||
token = session.pop("access_token", "asdf")
|
||||
session.clear()
|
||||
s = os.getenv("OIDC_CLIENT_ID") + ":" + os.getenv("OIDC_CLIENT_SECRET")
|
||||
r = requests.post(
|
||||
url = metadata["revocation_endpoint"],
|
||||
headers = {"Authorization": "Basic " + base64.b64encode(s.encode()).decode()},
|
||||
data = {"token": token, "token_type_hint": "access_token"}
|
||||
)
|
||||
if r.status_code != 200:
|
||||
return "oops", 500
|
||||
return redirect("/")
|
||||
|
||||
def read_user(token=None):
|
||||
token = token or session.get("access_token", False)
|
||||
if not token:
|
||||
return False
|
||||
r = requests.get(url = metadata["userinfo_endpoint"], headers = {
|
||||
"Authorization": "Bearer " + token
|
||||
})
|
||||
if r.status_code == 200:
|
||||
return r.json()
|
||||
else:
|
||||
return False
|
||||
|
||||
def validate_id_token(token=None):
|
||||
token = token or session.get("id_token", False)
|
||||
if not token:
|
||||
return False
|
||||
jwks_client = jwt.PyJWKClient(metadata["jwks_uri"])
|
||||
signing_key = jwks_client.get_signing_key_from_jwt(token)
|
||||
try:
|
||||
return jwt.decode(
|
||||
token,
|
||||
signing_key.key,
|
||||
algorithms=["RS256"],
|
||||
audience=os.getenv("OIDC_CLIENT_ID"),
|
||||
options={"verify_exp": True},
|
||||
)
|
||||
except jwt.InvalidTokenError as e:
|
||||
return False
|
||||
|
@ -16,7 +16,10 @@
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for item in items %}
|
||||
<tr>
|
||||
<tr {% if item | check_foreign_key_format %}
|
||||
style="background-color: coral;"
|
||||
{% endif %}
|
||||
>
|
||||
<td>
|
||||
{% if item.shortener %}
|
||||
<a href="http://k6.ee/{{ item.shortener.slug }}">{{ item.shortener.slug }}</a>
|
||||
|
@ -2,7 +2,7 @@
|
||||
|
||||
{% block content %}
|
||||
<div class="container">
|
||||
<h6>Please log in to see more details</h6>
|
||||
<h6>Please <a href="/login">log in</a> to see more details</h6>
|
||||
{% include "inventory_filter.html" %}
|
||||
<table>
|
||||
<thead>
|
||||
@ -15,7 +15,11 @@
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for item in items %}
|
||||
<tr>
|
||||
|
||||
<tr {% if item | check_foreign_key_format %}
|
||||
style="background-color: coral;"
|
||||
{% endif %}
|
||||
>
|
||||
<td>
|
||||
{% if item.shortener %}
|
||||
<a href="http://k6.ee/{{ item.shortener.slug }}">{{ item.shortener.slug }}</a>
|
||||
|
8
minio.yml
Normal file
8
minio.yml
Normal file
@ -0,0 +1,8 @@
|
||||
---
|
||||
apiVersion: codemowers.cloud/v1beta1
|
||||
kind: MinioBucketClaim
|
||||
metadata:
|
||||
name: inventory-app
|
||||
spec:
|
||||
capacity: 1Gi
|
||||
class: shared
|
@ -13,3 +13,5 @@ sepa
|
||||
Flask-WTF
|
||||
prometheus-flask-exporter
|
||||
pymongo
|
||||
pyjwt[crypto]
|
||||
kubernetes
|
||||
|
18
serviceaccount.yml
Normal file
18
serviceaccount.yml
Normal file
@ -0,0 +1,18 @@
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRoleBinding
|
||||
metadata:
|
||||
name: oidc-gateway-madis
|
||||
roleRef:
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
kind: ClusterRole
|
||||
name: cluster-admin
|
||||
subjects:
|
||||
- kind: ServiceAccount
|
||||
name: oidc-gateway
|
||||
namespace: hard2k1ll-72zn4
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: ServiceAccount
|
||||
metadata:
|
||||
name: oidc-gateway
|
20
skaffold.yaml
Normal file
20
skaffold.yaml
Normal file
@ -0,0 +1,20 @@
|
||||
apiVersion: skaffold/v3
|
||||
kind: Config
|
||||
metadata:
|
||||
name: inventory-app
|
||||
|
||||
deploy:
|
||||
kubectl: {}
|
||||
|
||||
manifests:
|
||||
rawYaml:
|
||||
- deployment.yaml
|
||||
|
||||
build:
|
||||
artifacts:
|
||||
- image: inventory-app
|
||||
sync:
|
||||
manual:
|
||||
- src: "inventory-app/**/*.{py,html}"
|
||||
dest: .
|
||||
strip: 'inventory-app'
|
Loading…
Reference in New Issue
Block a user