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):
|
def wrapper(f):
|
||||||
@wraps(f)
|
@wraps(f)
|
||||||
def decorated_function(*args, **kwargs):
|
def decorated_function(*args, **kwargs):
|
||||||
|
user = read_user()
|
||||||
d = {
|
d = {
|
||||||
"_id": ObjectId(kwargs.pop("item_id"))
|
"_id": ObjectId(kwargs.pop("item_id"))
|
||||||
}
|
}
|
||||||
if item_type:
|
if item_type:
|
||||||
d["type"] = item_type
|
d["type"] = item_type
|
||||||
if owner:
|
if owner:
|
||||||
d["inventory.owner.foreign_id"] = g.user["_id"]
|
d["inventory.owner.foreign_id"] = user["username"]
|
||||||
kwargs["item"] = db.inventory.find_one(d)
|
kwargs["item"] = db.inventory.find_one(d)
|
||||||
return f(*args, **kwargs)
|
return f(*args, **kwargs)
|
||||||
return decorated_function
|
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
|
import const
|
||||||
from common import CustomForm, build_query, flatten, format_name, spam
|
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__)
|
page_inventory = Blueprint("inventory", __name__)
|
||||||
db = MongoClient(const.MONGO_URI).get_default_database()
|
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):
|
def view_inventory_view(item_id):
|
||||||
template = "inventory_view.html"
|
template = "inventory_view.html"
|
||||||
item = db.inventory.find_one({ "_id": ObjectId(item_id) })
|
item = db.inventory.find_one({ "_id": ObjectId(item_id) })
|
||||||
if not has_login():
|
if not read_user():
|
||||||
if not item["inventory"].get("public"):
|
if not item["inventory"].get("public"):
|
||||||
return login_redirect()
|
return do_login()
|
||||||
template = "inventory_view_public.html"
|
template = "inventory_view_public.html"
|
||||||
base_url = const.INVENTORY_ASSETS_BASE_URL
|
base_url = const.INVENTORY_ASSETS_BASE_URL
|
||||||
photo_url = "%s/kspace-inventory/%s" % (base_url, item_id)
|
photo_url = "%s/kspace-inventory/%s" % (base_url, item_id)
|
||||||
@ -355,7 +355,7 @@ def view_inventory(slug=None):
|
|||||||
q = {"type": {"$ne": "token"}}
|
q = {"type": {"$ne": "token"}}
|
||||||
template = "inventory.html"
|
template = "inventory.html"
|
||||||
public_view = False
|
public_view = False
|
||||||
if not has_login():
|
if not read_user():
|
||||||
q.update({"inventory.public": True})
|
q.update({"inventory.public": True})
|
||||||
template = "inventory_public.html"
|
template = "inventory_public.html"
|
||||||
public_view = True
|
public_view = True
|
||||||
@ -409,28 +409,29 @@ def view_inventory(slug=None):
|
|||||||
{ "$match": q2 },
|
{ "$match": q2 },
|
||||||
{ "$sort": { sort_field_final : 1 if sort_direction == "asc" else -1 } }
|
{ "$sort": { sort_field_final : 1 if sort_direction == "asc" else -1 } }
|
||||||
])
|
])
|
||||||
|
|
||||||
return render_template(template, **locals())
|
return render_template(template, **locals())
|
||||||
|
|
||||||
|
|
||||||
@page_inventory.route("/m/inventory/<item_id>/claim", methods=["POST"])
|
@page_inventory.route("/m/inventory/<item_id>/claim", methods=["POST"])
|
||||||
@login_required
|
@login_required
|
||||||
def view_inventory_claim(item_id):
|
def view_inventory_claim(item_id):
|
||||||
|
user = read_user()
|
||||||
db.inventory.update_one({
|
db.inventory.update_one({
|
||||||
"_id": ObjectId(item_id),
|
"_id": ObjectId(item_id),
|
||||||
"inventory.owner.foreign_id": None
|
"inventory.owner.foreign_id": None
|
||||||
}, {
|
}, {
|
||||||
"$set": {
|
"$set": {
|
||||||
"inventory.owner.foreign_id": ObjectId(g.user["_id"]),
|
"inventory.owner.foreign_id": user["username"],
|
||||||
"inventory.owner.display_name": g.user["full_name"],
|
"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"])
|
@page_inventory.route("/m/inventory/<item_id>/use", methods=["POST"])
|
||||||
@login_required
|
@login_required
|
||||||
def view_inventory_use(item_id):
|
def view_inventory_use(item_id):
|
||||||
|
user = read_user()
|
||||||
item = db.inventory.find_one({
|
item = db.inventory.find_one({
|
||||||
"_id": ObjectId(item_id),
|
"_id": ObjectId(item_id),
|
||||||
"inventory.usable": True,
|
"inventory.usable": True,
|
||||||
@ -443,25 +444,26 @@ def view_inventory_use(item_id):
|
|||||||
"_id": ObjectId(item["_id"])
|
"_id": ObjectId(item["_id"])
|
||||||
}, {
|
}, {
|
||||||
"$set": {
|
"$set": {
|
||||||
"inventory.user.foreign_id": ObjectId(g.user["_id"]),
|
"inventory.user.foreign_id": ObjectId(user["username"]),
|
||||||
"inventory.user.display_name": g.user["full_name"],
|
"inventory.user.display_name": user["name"],
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
name = format_name(item)
|
item_name = format_name(item)
|
||||||
msg = "%s has started using %s" % (g.user["full_name"], name)
|
msg = "%s has started using %s" % (user["name"], item_name)
|
||||||
if item.get("shortener") and item["shortener"].get("slug"):
|
if item.get("shortener") and item["shortener"].get("slug"):
|
||||||
msg += ("\nk6.ee/%s" % item["shortener"]["slug"])
|
msg += ("\nk6.ee/%s" % item["shortener"]["slug"])
|
||||||
spam(msg)
|
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"])
|
@page_inventory.route("/m/inventory/<item_id>/vacate", methods=["POST"])
|
||||||
@login_required
|
@login_required
|
||||||
def view_inventory_vacate(item_id):
|
def view_inventory_vacate(item_id):
|
||||||
|
user = read_user()
|
||||||
item = db.inventory.find_one({
|
item = db.inventory.find_one({
|
||||||
"_id": ObjectId(item_id),
|
"_id": ObjectId(item_id),
|
||||||
"inventory.usable": True,
|
"inventory.usable": True,
|
||||||
"inventory.user.foreign_id": ObjectId(g.user["_id"])
|
"inventory.user.foreign_id": ObjectId(user["username"])
|
||||||
})
|
})
|
||||||
if not item:
|
if not item:
|
||||||
return abort(404)
|
return abort(404)
|
||||||
@ -473,9 +475,9 @@ def view_inventory_vacate(item_id):
|
|||||||
"inventory.user": ""
|
"inventory.user": ""
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
name = format_name(item)
|
item_name = format_name(item)
|
||||||
msg = "%s has stopped using %s" % (g.user["full_name"], name)
|
msg = "%s has stopped using %s" % (user["name"], item_name)
|
||||||
if item.get("shortener") and item["shortener"].get("slug"):
|
if item.get("shortener") and item["shortener"].get("slug"):
|
||||||
msg += ("\nk6.ee/%s" % item["shortener"]["slug"])
|
msg += ("\nk6.ee/%s" % item["shortener"]["slug"])
|
||||||
spam(msg)
|
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
|
import const
|
||||||
from common import CustomForm, devenv, flatten, format_name, spam
|
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 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):
|
def render_markdown(text):
|
||||||
if not 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['user_link'] = render_user_link
|
||||||
jinja2.filters.FILTERS['is_list'] = is_list
|
jinja2.filters.FILTERS['is_list'] = is_list
|
||||||
jinja2.filters.FILTERS['quote_plus'] = lambda u: urllib.parse.quote_plus(u)
|
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/'))
|
env = Environment(loader=FileSystemLoader('templates/'))
|
||||||
|
|
||||||
|
|
||||||
@ -110,6 +122,8 @@ class ReverseProxied(object):
|
|||||||
app = Flask(__name__)
|
app = Flask(__name__)
|
||||||
app.wsgi_app = ReverseProxied(app.wsgi_app)
|
app.wsgi_app = ReverseProxied(app.wsgi_app)
|
||||||
app.register_blueprint(page_inventory)
|
app.register_blueprint(page_inventory)
|
||||||
|
app.register_blueprint(page_oidc)
|
||||||
|
app.register_blueprint(page_api)
|
||||||
metrics = PrometheusMetrics(app, group_by="path")
|
metrics = PrometheusMetrics(app, group_by="path")
|
||||||
|
|
||||||
app.config['SECRET_KEY'] = const.SECRET_KEY
|
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):
|
if any(c.isdigit() for c in field.data):
|
||||||
raise ValidationError("Name must not contain numbers")
|
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")
|
@app.route("/login")
|
||||||
def view_login():
|
@login_required
|
||||||
form = LoginLinkForm()
|
def login_dummy():
|
||||||
return render_template("login.html", **locals())
|
return redirect("/m/inventory")
|
||||||
|
|
||||||
|
@app.route("/")
|
||||||
|
def index():
|
||||||
|
return redirect("/m/inventory")
|
||||||
|
|
||||||
@app.route("/login/authelia")
|
@app.route("/me")
|
||||||
def view_login_authelia():
|
@login_required
|
||||||
redirect_location = session.pop('target_path', "/m/profile")
|
def view_profile():
|
||||||
username = request.headers.get("Remote-User")
|
user = read_user()
|
||||||
if not username:
|
return f"Hello {user['name']}"
|
||||||
raise ValueError("Ding dong")
|
|
||||||
|
|
||||||
member = mongodb.member.find_one({"ad.username": username, "enabled":True})
|
@app.route("/hello")
|
||||||
if not member:
|
def view_hello():
|
||||||
return render_template("login_error.html")
|
return "Hello!"
|
||||||
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
|
|
||||||
|
|
||||||
class MultiCheckboxField(SelectMultipleField):
|
class MultiCheckboxField(SelectMultipleField):
|
||||||
widget = widgets.ListWidget(prefix_label=False)
|
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>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{% for item in items %}
|
{% for item in items %}
|
||||||
<tr>
|
<tr {% if item | check_foreign_key_format %}
|
||||||
|
style="background-color: coral;"
|
||||||
|
{% endif %}
|
||||||
|
>
|
||||||
<td>
|
<td>
|
||||||
{% if item.shortener %}
|
{% if item.shortener %}
|
||||||
<a href="http://k6.ee/{{ item.shortener.slug }}">{{ item.shortener.slug }}</a>
|
<a href="http://k6.ee/{{ item.shortener.slug }}">{{ item.shortener.slug }}</a>
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="container">
|
<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" %}
|
{% include "inventory_filter.html" %}
|
||||||
<table>
|
<table>
|
||||||
<thead>
|
<thead>
|
||||||
@ -15,7 +15,11 @@
|
|||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{% for item in items %}
|
{% for item in items %}
|
||||||
<tr>
|
|
||||||
|
<tr {% if item | check_foreign_key_format %}
|
||||||
|
style="background-color: coral;"
|
||||||
|
{% endif %}
|
||||||
|
>
|
||||||
<td>
|
<td>
|
||||||
{% if item.shortener %}
|
{% if item.shortener %}
|
||||||
<a href="http://k6.ee/{{ item.shortener.slug }}">{{ item.shortener.slug }}</a>
|
<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
|
Flask-WTF
|
||||||
prometheus-flask-exporter
|
prometheus-flask-exporter
|
||||||
pymongo
|
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