Compare commits
No commits in common. "b06757fb03f5f785190d8c7bee5d03b3cf4ee23d" and "cf02c3a769c10fda19daf37ba43b90ad50bb6056" have entirely different histories.
b06757fb03
...
cf02c3a769
2
.gitignore
vendored
2
.gitignore
vendored
@ -1,2 +0,0 @@
|
|||||||
*.sw*
|
|
||||||
.DS_Store
|
|
@ -1,4 +1,4 @@
|
|||||||
steps:
|
pipeline:
|
||||||
buildweb:
|
buildweb:
|
||||||
group: build
|
group: build
|
||||||
image: node
|
image: node
|
||||||
@ -44,5 +44,4 @@ steps:
|
|||||||
- s3cmd -c /root/.s3cfg sync --no-mime-magic --guess-mime-type dist/* s3://sso.tools
|
- s3cmd -c /root/.s3cfg sync --no-mime-magic --guess-mime-type dist/* s3://sso.tools
|
||||||
- 'curl -X POST -H "AccessKey: $BUNNY_KEY" https://api.bunny.net/pullzone/782714/purgeCache'
|
- 'curl -X POST -H "AccessKey: $BUNNY_KEY" https://api.bunny.net/pullzone/782714/purgeCache'
|
||||||
|
|
||||||
when:
|
branches: main
|
||||||
branch: main
|
|
@ -1,4 +1,4 @@
|
|||||||
FROM python:3.12-slim-bookworm
|
FROM python:3.9-slim-buster
|
||||||
|
|
||||||
# set work directory
|
# set work directory
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
@ -7,7 +7,7 @@ This directory contains the source code for the primary API for the SSO Tools we
|
|||||||
Firstly, create and enter a virtual environment:
|
Firstly, create and enter a virtual environment:
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
virtualenv -p python3.12 .venv # You only need this the first time
|
virtualenv -p python3 .venv # You only need this the first time
|
||||||
source .venv/bin/activate
|
source .venv/bin/activate
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@ -1,223 +0,0 @@
|
|||||||
import datetime
|
|
||||||
import jwt
|
|
||||||
import bcrypt
|
|
||||||
import os
|
|
||||||
from bson.objectid import ObjectId
|
|
||||||
from util import database, mail, errors
|
|
||||||
|
|
||||||
jwt_secret = os.environ.get("JWT_SECRET")
|
|
||||||
|
|
||||||
|
|
||||||
def create(data):
|
|
||||||
email = data.get("email")
|
|
||||||
first_name = data.get("firstName")
|
|
||||||
last_name = data.get("lastName")
|
|
||||||
password = data.get("password")
|
|
||||||
if not email or len(email) < 6:
|
|
||||||
raise errors.BadRequest("Your name or email is too short or invalid.")
|
|
||||||
if not password or len(password) < 8:
|
|
||||||
raise errors.BadRequest("Your password should be at least 8 characters.")
|
|
||||||
email = email.lower()
|
|
||||||
idps_to_claim = data.get("idpsToClaim", []) or []
|
|
||||||
idps_to_claim = list(map(lambda i: ObjectId(i), idps_to_claim))
|
|
||||||
db = database.get_db()
|
|
||||||
existingUser = db.users.find_one({"email": email})
|
|
||||||
if existingUser:
|
|
||||||
raise errors.BadRequest("An account with this email already exists.")
|
|
||||||
|
|
||||||
hashed_password = bcrypt.hashpw(password.encode("utf-8"), bcrypt.gensalt())
|
|
||||||
new_user = {
|
|
||||||
"firstName": first_name,
|
|
||||||
"lastName": last_name,
|
|
||||||
"email": email,
|
|
||||||
"password": hashed_password,
|
|
||||||
"createdAt": datetime.datetime.utcnow(),
|
|
||||||
}
|
|
||||||
result = db.users.insert_one(new_user)
|
|
||||||
new_user["_id"] = result.inserted_id
|
|
||||||
if len(idps_to_claim):
|
|
||||||
db.idps.update_many(
|
|
||||||
{"_id": {"$in": idps_to_claim}, "user": {"$exists": False}},
|
|
||||||
{"$set": {"user": new_user["_id"]}},
|
|
||||||
)
|
|
||||||
mail.send(
|
|
||||||
{
|
|
||||||
"to": os.environ.get("ADMIN_EMAIL"),
|
|
||||||
"subject": "{} signup".format(os.environ.get("APP_NAME")),
|
|
||||||
"text": "A new user signed up with email {0}".format(email),
|
|
||||||
}
|
|
||||||
)
|
|
||||||
return {"token": generate_access_token(new_user["_id"])}
|
|
||||||
|
|
||||||
|
|
||||||
def enrol(data):
|
|
||||||
if not data or "token" not in data or "password" not in data:
|
|
||||||
raise errors.BadRequest("Invalid request")
|
|
||||||
token = data.get("token")
|
|
||||||
password = data.get("password")
|
|
||||||
if not token:
|
|
||||||
raise errors.BadRequest("Invalid token")
|
|
||||||
if not password or len(password) < 8:
|
|
||||||
raise errors.BadRequest("Your password should be at least 8 characters.")
|
|
||||||
try:
|
|
||||||
db = database.get_db()
|
|
||||||
id = jwt.decode(data["token"], jwt_secret)["sub"]
|
|
||||||
user = db.users.find_one({"_id": ObjectId(id), "tokens.enrolment": token})
|
|
||||||
hashed_password = bcrypt.hashpw(password.encode("utf-8"), bcrypt.gensalt())
|
|
||||||
db.users.update_one(
|
|
||||||
{"_id": user["_id"]},
|
|
||||||
{"$set": {"password": hashed_password}, "$unset": {"tokens.enrolment": ""}},
|
|
||||||
)
|
|
||||||
return {"token": generate_access_token(user["_id"])}
|
|
||||||
except Exception as e:
|
|
||||||
print(e)
|
|
||||||
raise errors.BadRequest(
|
|
||||||
"Unable to enrol your account. Your token may be invalid or expired."
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def login(data):
|
|
||||||
email = data.get("email")
|
|
||||||
password = data.get("password")
|
|
||||||
idps_to_claim = data.get("idpsToClaim", []) or []
|
|
||||||
idps_to_claim = list(map(lambda i: ObjectId(i), idps_to_claim))
|
|
||||||
|
|
||||||
db = database.get_db()
|
|
||||||
user = db.users.find_one({"email": email.lower()})
|
|
||||||
try:
|
|
||||||
if user and bcrypt.checkpw(password.encode("utf-8"), user["password"]):
|
|
||||||
if len(idps_to_claim):
|
|
||||||
db.idps.update_many(
|
|
||||||
{"_id": {"$in": idps_to_claim}, "user": {"$exists": False}},
|
|
||||||
{"$set": {"user": user["_id"]}},
|
|
||||||
)
|
|
||||||
return {"token": generate_access_token(user["_id"])}
|
|
||||||
else:
|
|
||||||
raise errors.BadRequest("Your email or password is incorrect.")
|
|
||||||
except Exception as e:
|
|
||||||
print(e)
|
|
||||||
raise errors.BadRequest("Your email or password is incorrect.")
|
|
||||||
|
|
||||||
|
|
||||||
def logout(user):
|
|
||||||
db = database.get_db()
|
|
||||||
db.users.update_one(
|
|
||||||
{"_id": user["_id"]}, {"$pull": {"tokens.login": user["currentToken"]}}
|
|
||||||
)
|
|
||||||
return {"loggedOut": True}
|
|
||||||
|
|
||||||
|
|
||||||
def update_password(user, data):
|
|
||||||
if not data:
|
|
||||||
raise errors.BadRequest("Invalid request")
|
|
||||||
if "newPassword" not in data:
|
|
||||||
raise errors.BadRequest("Invalid request")
|
|
||||||
if len(data["newPassword"]) < 8:
|
|
||||||
raise errors.BadRequest("New password is too short")
|
|
||||||
|
|
||||||
db = database.get_db()
|
|
||||||
if "currentPassword" in data:
|
|
||||||
if not bcrypt.checkpw(
|
|
||||||
data["currentPassword"].encode("utf-8"), user["password"]
|
|
||||||
):
|
|
||||||
raise errors.BadRequest("Incorrect password")
|
|
||||||
elif "token" in data:
|
|
||||||
try:
|
|
||||||
id = jwt.decode(data["token"], jwt_secret, algorithms=["HS256"])["sub"]
|
|
||||||
user = db.users.find_one(
|
|
||||||
{"_id": ObjectId(id), "tokens.passwordReset": data["token"]}
|
|
||||||
)
|
|
||||||
if not user:
|
|
||||||
raise Exception
|
|
||||||
except Exception as e:
|
|
||||||
print(e)
|
|
||||||
raise errors.BadRequest(
|
|
||||||
"There was a problem updating your password. Your token may be invalid or out of date"
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
raise errors.BadRequest("Current password or reset token is required")
|
|
||||||
if not user:
|
|
||||||
raise errors.BadRequest("Unable to change your password")
|
|
||||||
|
|
||||||
hashed_password = bcrypt.hashpw(
|
|
||||||
data["newPassword"].encode("utf-8"), bcrypt.gensalt()
|
|
||||||
)
|
|
||||||
db.users.update_one(
|
|
||||||
{"_id": user["_id"]},
|
|
||||||
{"$set": {"password": hashed_password}, "$unset": {"tokens.passwordReset": ""}},
|
|
||||||
)
|
|
||||||
return {"passwordUpdated": True}
|
|
||||||
|
|
||||||
|
|
||||||
def delete(user, data):
|
|
||||||
password = data.get("password")
|
|
||||||
if not password or not bcrypt.checkpw(password.encode("utf-8"), user["password"]):
|
|
||||||
raise errors.BadRequest("Incorrect password")
|
|
||||||
db = database.get_db()
|
|
||||||
for idp in db.idps.find({"user": user["_id"]}):
|
|
||||||
db.idpSps.delete_many({"idp": idp["_id"]})
|
|
||||||
db.idpUsers.delete_many({"idp": idp["_id"]})
|
|
||||||
db.idpAttributes.delete_many({"idp": idp["_id"]})
|
|
||||||
db.idps.delete_many({"user": user["_id"]})
|
|
||||||
db.users.delete_one({"_id": user["_id"]})
|
|
||||||
return {"deletedUser": user["_id"]}
|
|
||||||
|
|
||||||
|
|
||||||
def generate_access_token(user_id):
|
|
||||||
payload = {
|
|
||||||
"exp": datetime.datetime.utcnow() + datetime.timedelta(days=30),
|
|
||||||
"iat": datetime.datetime.utcnow(),
|
|
||||||
"sub": str(user_id),
|
|
||||||
}
|
|
||||||
token = jwt.encode(payload, jwt_secret, algorithm="HS256")
|
|
||||||
db = database.get_db()
|
|
||||||
db.users.update_one({"_id": user_id}, {"$addToSet": {"tokens.login": token}})
|
|
||||||
return token
|
|
||||||
|
|
||||||
|
|
||||||
def get_user_context(token):
|
|
||||||
if not token:
|
|
||||||
return None
|
|
||||||
try:
|
|
||||||
payload = jwt.decode(token, jwt_secret, algorithms=["HS256"])
|
|
||||||
id = payload["sub"]
|
|
||||||
if id:
|
|
||||||
db = database.get_db()
|
|
||||||
user = db.users.find_one({"_id": ObjectId(id), "tokens.login": token})
|
|
||||||
db.users.update_one(
|
|
||||||
{"_id": user["_id"]}, {"$set": {"lastSeenAt": datetime.datetime.now()}}
|
|
||||||
)
|
|
||||||
user["currentToken"] = token
|
|
||||||
return user
|
|
||||||
except Exception as e:
|
|
||||||
print(e)
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
def reset_password(data):
|
|
||||||
if not data or "email" not in data:
|
|
||||||
raise errors.BadRequest("Invalid request")
|
|
||||||
if len(data["email"]) < 5:
|
|
||||||
raise errors.BadRequest("Your email is too short")
|
|
||||||
db = database.get_db()
|
|
||||||
user = db.users.find_one({"email": data["email"].lower()})
|
|
||||||
if user:
|
|
||||||
payload = {
|
|
||||||
"exp": datetime.datetime.utcnow() + datetime.timedelta(days=1),
|
|
||||||
"iat": datetime.datetime.utcnow(),
|
|
||||||
"sub": str(user["_id"]),
|
|
||||||
}
|
|
||||||
token = jwt.encode(payload, jwt_secret, algorithm="HS256")
|
|
||||||
mail.send(
|
|
||||||
{
|
|
||||||
"to_user": user,
|
|
||||||
"subject": "Reset your password",
|
|
||||||
"text": "Dear {0},\n\nA password reset email was recently requested for your SSO Tools account. If this was you and you want to continue, please follow the link below:\n\n{1}\n\nThis link will expire after 24 hours.\n\nIf this was not you, then someone may be trying to gain access to your account. We recommend using a strong and unique password for your account.".format(
|
|
||||||
user["firstName"], "https://sso.tools/password/reset?token=" + token
|
|
||||||
),
|
|
||||||
}
|
|
||||||
)
|
|
||||||
db.users.update_one(
|
|
||||||
{"_id": user["_id"]}, {"$set": {"tokens.passwordReset": token}}
|
|
||||||
)
|
|
||||||
return {"passwordResetEmailSent": True}
|
|
502
api/api/idps.py
502
api/api/idps.py
@ -1,502 +0,0 @@
|
|||||||
from uuid import uuid4
|
|
||||||
import bcrypt
|
|
||||||
import re
|
|
||||||
import random
|
|
||||||
import string
|
|
||||||
from OpenSSL import crypto
|
|
||||||
import pymongo
|
|
||||||
from bson.objectid import ObjectId
|
|
||||||
from util import database, errors, util
|
|
||||||
|
|
||||||
forbidden_codes = [
|
|
||||||
"",
|
|
||||||
"app",
|
|
||||||
"my",
|
|
||||||
"www",
|
|
||||||
"support",
|
|
||||||
"mail",
|
|
||||||
"email",
|
|
||||||
"dashboard",
|
|
||||||
"ssotools",
|
|
||||||
"myidp",
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
def create_self_signed_cert(name):
|
|
||||||
# create a key pair
|
|
||||||
k = crypto.PKey()
|
|
||||||
k.generate_key(crypto.TYPE_RSA, 1024)
|
|
||||||
|
|
||||||
# create a self-signed cert
|
|
||||||
cert = crypto.X509()
|
|
||||||
cert.get_subject().C = "GB"
|
|
||||||
cert.get_subject().O = "SSO Tools"
|
|
||||||
cert.get_subject().OU = name
|
|
||||||
cert.gmtime_adj_notBefore(0)
|
|
||||||
cert.gmtime_adj_notAfter(20 * 365 * 24 * 60 * 60)
|
|
||||||
cert.set_issuer(cert.get_subject())
|
|
||||||
cert.set_pubkey(k)
|
|
||||||
cert.sign(k, "sha256")
|
|
||||||
dumped = crypto.dump_certificate(crypto.FILETYPE_PEM, cert)
|
|
||||||
dumped_key = crypto.dump_privatekey(crypto.FILETYPE_PEM, k)
|
|
||||||
return {"cert": dumped, "key": dumped_key}
|
|
||||||
|
|
||||||
|
|
||||||
def can_manage_idp(user, idp):
|
|
||||||
if not idp:
|
|
||||||
return False
|
|
||||||
if not user:
|
|
||||||
return not idp.get("user")
|
|
||||||
if user:
|
|
||||||
return (
|
|
||||||
util.has_permission(user, "root")
|
|
||||||
or idp.get("user") == user["_id"]
|
|
||||||
or not idp.get("user")
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def create(user, data):
|
|
||||||
db = database.get_db()
|
|
||||||
if not data or not data.get("code") or not data.get("name"):
|
|
||||||
return errors.BadRequest("Name and issuer are required")
|
|
||||||
code = data.get("code").lower().strip()
|
|
||||||
|
|
||||||
if not len(code) or code in forbidden_codes or db.idps.find_one({"code": code}):
|
|
||||||
raise errors.BadRequest("The issuer is invalid or is already in use")
|
|
||||||
if not re.match(r"^([a-zA-Z0-9\-_]+)$", code):
|
|
||||||
raise errors.BadRequest(
|
|
||||||
"The IdP issuer is invalid. Please just use letters, numbers, hyphens, and underscores."
|
|
||||||
)
|
|
||||||
|
|
||||||
x509 = create_self_signed_cert(data["name"])
|
|
||||||
idp = {
|
|
||||||
"name": data.get("name"),
|
|
||||||
"code": code,
|
|
||||||
"saml": {
|
|
||||||
"certificate": x509["cert"].decode("utf-8"),
|
|
||||||
"privateKey": x509["key"].decode("utf-8"),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
if user:
|
|
||||||
idp["user"] = user["_id"]
|
|
||||||
result = db.idps.insert_one(idp)
|
|
||||||
idp["_id"] = result.inserted_id
|
|
||||||
|
|
||||||
create_user(
|
|
||||||
user,
|
|
||||||
idp["_id"],
|
|
||||||
{
|
|
||||||
"email": "joe@example.com",
|
|
||||||
"firstName": "Joe",
|
|
||||||
"lastName": "Bloggs",
|
|
||||||
"password": "password",
|
|
||||||
},
|
|
||||||
)
|
|
||||||
create_user(
|
|
||||||
user,
|
|
||||||
idp["_id"],
|
|
||||||
{
|
|
||||||
"email": "jane@example.com",
|
|
||||||
"firstName": "Jane",
|
|
||||||
"lastName": "Doe",
|
|
||||||
"password": "password",
|
|
||||||
},
|
|
||||||
)
|
|
||||||
create_attribute(
|
|
||||||
user,
|
|
||||||
idp["_id"],
|
|
||||||
{"name": "Group", "defaultValue": "staff", "samlMapping": "group"},
|
|
||||||
)
|
|
||||||
|
|
||||||
return idp
|
|
||||||
|
|
||||||
|
|
||||||
def get(user, include):
|
|
||||||
db = database.get_db()
|
|
||||||
if include:
|
|
||||||
include = list(map(lambda i: ObjectId(i), include.split(",")))
|
|
||||||
else:
|
|
||||||
include = []
|
|
||||||
|
|
||||||
if user:
|
|
||||||
query = {
|
|
||||||
"$or": [
|
|
||||||
{"user": user["_id"]},
|
|
||||||
{"_id": {"$in": include}, "user": {"$exists": False}},
|
|
||||||
]
|
|
||||||
}
|
|
||||||
else:
|
|
||||||
query = {"_id": {"$in": include}, "user": {"$exists": False}}
|
|
||||||
idps = list(db.idps.find(query, {"name": 1, "code": 1}))
|
|
||||||
return {"idps": idps}
|
|
||||||
|
|
||||||
|
|
||||||
def get_one(user, id):
|
|
||||||
id = ObjectId(id)
|
|
||||||
db = database.get_db()
|
|
||||||
idp = db.idps.find_one(id)
|
|
||||||
if not idp:
|
|
||||||
raise errors.NotFound("The IdP could not be found")
|
|
||||||
if not can_manage_idp(user, idp):
|
|
||||||
raise errors.Forbidden("You can't view this IdP")
|
|
||||||
idp["users"] = list(
|
|
||||||
db.idpUsers.find(
|
|
||||||
{"idp": idp["_id"]}, {"firstName": 1, "lastName": 1, "email": 1}
|
|
||||||
)
|
|
||||||
)
|
|
||||||
return idp
|
|
||||||
|
|
||||||
|
|
||||||
def update(user, id, data):
|
|
||||||
id = ObjectId(id)
|
|
||||||
db = database.get_db()
|
|
||||||
idp = db.idps.find_one(id)
|
|
||||||
if not idp:
|
|
||||||
return errors.NotFound("IDP not found")
|
|
||||||
if not can_manage_idp(user, idp):
|
|
||||||
raise errors.Forbidden("You can't edit this IdP")
|
|
||||||
|
|
||||||
if "code" in data:
|
|
||||||
code = data.get("code").lower().strip()
|
|
||||||
if (
|
|
||||||
not len(code)
|
|
||||||
or code in forbidden_codes
|
|
||||||
or db.idps.find_one({"code": code, "_id": {"$ne": id}})
|
|
||||||
):
|
|
||||||
raise errors.BadRequest("This code is invalid or is already in use")
|
|
||||||
if not re.match(r"^([a-zA-Z0-9\-_]+)$", code):
|
|
||||||
raise errors.BadRequest(
|
|
||||||
"The IdP issuer is invalid. Please just use letters, numbers, hyphens, and underscores."
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
code = idp["code"]
|
|
||||||
update_data = {"name": data.get("name", idp["name"]), "code": code}
|
|
||||||
db.idps.update_one({"_id": id}, {"$set": update_data})
|
|
||||||
return get_one(user, id)
|
|
||||||
|
|
||||||
|
|
||||||
def delete(user, id):
|
|
||||||
id = ObjectId(id)
|
|
||||||
db = database.get_db()
|
|
||||||
idp = db.idps.find_one(id)
|
|
||||||
if not idp:
|
|
||||||
return errors.NotFound("IDP not found")
|
|
||||||
if not can_manage_idp(user, idp):
|
|
||||||
raise errors.Forbidden("You can't delete this IdP")
|
|
||||||
db.idps.delete_one({"_id": id})
|
|
||||||
return {"deletedIDP": id}
|
|
||||||
|
|
||||||
|
|
||||||
#### SPs
|
|
||||||
|
|
||||||
|
|
||||||
def create_sp(user, id, data):
|
|
||||||
id = ObjectId(id)
|
|
||||||
db = database.get_db()
|
|
||||||
idp = db.idps.find_one(id)
|
|
||||||
if not idp:
|
|
||||||
return errors.NotFound("IDP not found")
|
|
||||||
if not can_manage_idp(user, idp):
|
|
||||||
raise errors.Forbidden("You can't manage this IdP")
|
|
||||||
|
|
||||||
sp = {
|
|
||||||
"name": data.get("name"),
|
|
||||||
"type": "saml",
|
|
||||||
"entityId": data.get("entityId"),
|
|
||||||
"serviceUrl": data.get("serviceUrl"),
|
|
||||||
"callbackUrl": data.get("callbackUrl"),
|
|
||||||
"logoutUrl": data.get("logoutUrl"),
|
|
||||||
"logoutCallbackUrl": data.get("logoutCallbackUrl"),
|
|
||||||
"oauth2ClientId": str(uuid4()),
|
|
||||||
"oauth2ClientSecret": str(
|
|
||||||
"".join(random.choices(string.ascii_uppercase + string.digits, k=32))
|
|
||||||
),
|
|
||||||
"oauth2RedirectUri": data.get("oauth2RedirectUri"),
|
|
||||||
"idp": id,
|
|
||||||
}
|
|
||||||
result = db.idpSps.insert_one(sp)
|
|
||||||
sp["_id"] = result.inserted_id
|
|
||||||
return sp
|
|
||||||
|
|
||||||
|
|
||||||
def update_sp(user, id, sp_id, data):
|
|
||||||
id = ObjectId(id)
|
|
||||||
sp_id = ObjectId(sp_id)
|
|
||||||
db = database.get_db()
|
|
||||||
existing = db.idpSps.find_one(sp_id)
|
|
||||||
if not existing:
|
|
||||||
return errors.NotFound("SP not found")
|
|
||||||
idp = db.idps.find_one(existing["idp"])
|
|
||||||
if not idp:
|
|
||||||
return errors.NotFound("IDP not found")
|
|
||||||
if not can_manage_idp(user, idp):
|
|
||||||
raise errors.Forbidden("You can't update this IdP")
|
|
||||||
|
|
||||||
update_data = {
|
|
||||||
"name": data.get("name"),
|
|
||||||
"entityId": data.get("entityId"),
|
|
||||||
"serviceUrl": data.get("serviceUrl"),
|
|
||||||
"callbackUrl": data.get("callbackUrl"),
|
|
||||||
"logoutUrl": data.get("logoutUrl"),
|
|
||||||
"logoutCallbackUrl": data.get("logoutCallbackUrl"),
|
|
||||||
"oauth2RedirectUri": data.get("oauth2RedirectUri"),
|
|
||||||
}
|
|
||||||
db.idpSps.update_one({"_id": sp_id}, {"$set": update_data})
|
|
||||||
return db.idpSps.find_one(
|
|
||||||
{"_id": sp_id},
|
|
||||||
{
|
|
||||||
"name": 1,
|
|
||||||
"entityId": 1,
|
|
||||||
"serviceUrl": 1,
|
|
||||||
"callbackUrl": 1,
|
|
||||||
"logoutUrl": 1,
|
|
||||||
"logoutCallbackUrl": 1,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def get_sps(user, id):
|
|
||||||
id = ObjectId(id)
|
|
||||||
db = database.get_db()
|
|
||||||
idp = db.idps.find_one(id)
|
|
||||||
if not idp:
|
|
||||||
return errors.NotFound("IDP not found")
|
|
||||||
if not can_manage_idp(user, idp):
|
|
||||||
raise errors.Forbidden("You can't update this IdP")
|
|
||||||
|
|
||||||
sps = list(db.idpSps.find({"idp": id}))
|
|
||||||
return {"sps": sps}
|
|
||||||
|
|
||||||
|
|
||||||
def delete_sp(user, id, sp_id):
|
|
||||||
id = ObjectId(id)
|
|
||||||
sp_id = ObjectId(sp_id)
|
|
||||||
db = database.get_db()
|
|
||||||
|
|
||||||
existing = db.idpSps.find_one(sp_id)
|
|
||||||
if not existing:
|
|
||||||
return errors.NotFound("SP not found")
|
|
||||||
|
|
||||||
idp = db.idps.find_one(existing["idp"])
|
|
||||||
if not idp:
|
|
||||||
return errors.NotFound("IDP not found")
|
|
||||||
if not can_manage_idp(user, idp):
|
|
||||||
raise errors.Forbidden("You can't update this IdP")
|
|
||||||
|
|
||||||
db.idpSps.delete_one({"_id": sp_id})
|
|
||||||
return {"deletedIDPSP": sp_id}
|
|
||||||
|
|
||||||
|
|
||||||
#### Users
|
|
||||||
|
|
||||||
|
|
||||||
def create_user(user, id, data):
|
|
||||||
id = ObjectId(id)
|
|
||||||
db = database.get_db()
|
|
||||||
idp = db.idps.find_one(id)
|
|
||||||
if not idp:
|
|
||||||
return errors.NotFound("IDP not found")
|
|
||||||
if not can_manage_idp(user, idp):
|
|
||||||
raise errors.Forbidden("You can't update this IdP")
|
|
||||||
|
|
||||||
password = data["password"]
|
|
||||||
hashed_password = bcrypt.hashpw(password.encode("utf-8"), bcrypt.gensalt())
|
|
||||||
new_user = {
|
|
||||||
"firstName": data.get("firstName"),
|
|
||||||
"lastName": data.get("lastName"),
|
|
||||||
"email": data["email"],
|
|
||||||
"password": hashed_password,
|
|
||||||
"attributes": data.get("attributes", {}),
|
|
||||||
"idp": id,
|
|
||||||
}
|
|
||||||
|
|
||||||
result = db.idpUsers.insert_one(new_user)
|
|
||||||
new_user["_id"] = result.inserted_id
|
|
||||||
del new_user["password"]
|
|
||||||
return new_user
|
|
||||||
|
|
||||||
|
|
||||||
def update_user(user, id, user_id, data):
|
|
||||||
id = ObjectId(id)
|
|
||||||
user_id = ObjectId(user_id)
|
|
||||||
db = database.get_db()
|
|
||||||
existing = db.idpUsers.find_one(user_id)
|
|
||||||
if not existing:
|
|
||||||
return errors.NotFound("User not found")
|
|
||||||
idp = db.idps.find_one(existing["idp"])
|
|
||||||
if not idp:
|
|
||||||
return errors.NotFound("IDP not found")
|
|
||||||
if not can_manage_idp(user, idp):
|
|
||||||
raise errors.Forbidden("You can't update this IdP")
|
|
||||||
|
|
||||||
update_data = {
|
|
||||||
"firstName": data.get("firstName", existing.get("firstName")),
|
|
||||||
"lastName": data.get("lastName", existing.get("lastName")),
|
|
||||||
"email": data.get("email", existing.get("email")),
|
|
||||||
"attributes": data.get("attributes", existing.get("attributes", {})),
|
|
||||||
}
|
|
||||||
if data.get("password"):
|
|
||||||
hashed_password = bcrypt.hashpw(
|
|
||||||
data["password"].encode("utf-8"), bcrypt.gensalt()
|
|
||||||
)
|
|
||||||
update_data["password"] = hashed_password
|
|
||||||
db.idpUsers.update_one({"_id": user_id}, {"$set": update_data})
|
|
||||||
return db.idpUsers.find_one(
|
|
||||||
{"_id": user_id}, {"firstName": 1, "lastName": 1, "email": 1, "attributes": 1}
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def get_users(user, id):
|
|
||||||
id = ObjectId(id)
|
|
||||||
db = database.get_db()
|
|
||||||
idp = db.idps.find_one(id)
|
|
||||||
if not idp:
|
|
||||||
return errors.NotFound("IDP not found")
|
|
||||||
if not can_manage_idp(user, idp):
|
|
||||||
raise errors.Forbidden("You can't manage this IdP")
|
|
||||||
|
|
||||||
users = list(
|
|
||||||
db.idpUsers.find(
|
|
||||||
{"idp": id}, {"firstName": 1, "lastName": 1, "email": 1, "attributes": 1}
|
|
||||||
)
|
|
||||||
)
|
|
||||||
return {"users": users}
|
|
||||||
|
|
||||||
|
|
||||||
def delete_user(user, id, user_id):
|
|
||||||
id = ObjectId(id)
|
|
||||||
user_id = ObjectId(user_id)
|
|
||||||
db = database.get_db()
|
|
||||||
existing = db.idpUsers.find_one(user_id)
|
|
||||||
if not existing:
|
|
||||||
return errors.NotFound("User not found")
|
|
||||||
idp = db.idps.find_one(existing["idp"])
|
|
||||||
if not idp:
|
|
||||||
return errors.NotFound("IDP not found")
|
|
||||||
if not can_manage_idp(user, idp):
|
|
||||||
raise errors.Forbidden("You can't update this IdP")
|
|
||||||
|
|
||||||
db.idpUsers.delete_one({"_id": user_id})
|
|
||||||
return {"deletedUser": user_id}
|
|
||||||
|
|
||||||
|
|
||||||
#### Attributes
|
|
||||||
|
|
||||||
|
|
||||||
def create_attribute(user, id, data):
|
|
||||||
id = ObjectId(id)
|
|
||||||
db = database.get_db()
|
|
||||||
idp = db.idps.find_one(id)
|
|
||||||
if not idp:
|
|
||||||
return errors.NotFound("IDP not found")
|
|
||||||
if not can_manage_idp(user, idp):
|
|
||||||
raise errors.Forbidden("You can't update this IdP")
|
|
||||||
if not data or "name" not in data:
|
|
||||||
return errors.BadRequest("Attribute name is required")
|
|
||||||
|
|
||||||
new_attr = {
|
|
||||||
"name": data.get("name"),
|
|
||||||
"defaultValue": data.get("defaultValue"),
|
|
||||||
"samlMapping": data.get("samlMapping"),
|
|
||||||
"idp": id,
|
|
||||||
}
|
|
||||||
|
|
||||||
result = db.idpAttributes.insert_one(new_attr)
|
|
||||||
new_attr["_id"] = result.inserted_id
|
|
||||||
return new_attr
|
|
||||||
|
|
||||||
|
|
||||||
def update_attribute(user, id, attr_id, data):
|
|
||||||
id = ObjectId(id)
|
|
||||||
attr_id = ObjectId(attr_id)
|
|
||||||
db = database.get_db()
|
|
||||||
existing = db.idpAttributes.find_one(attr_id)
|
|
||||||
if not existing:
|
|
||||||
return errors.NotFound("Attribute not found")
|
|
||||||
idp = db.idps.find_one(existing["idp"])
|
|
||||||
if not idp:
|
|
||||||
return errors.NotFound("IDP not found")
|
|
||||||
if not can_manage_idp(user, idp):
|
|
||||||
raise errors.Forbidden("You can't update this IdP")
|
|
||||||
|
|
||||||
update_data = {
|
|
||||||
"name": data.get("name", existing.get("name")),
|
|
||||||
"defaultValue": data.get("defaultValue", existing.get("defaultValue")),
|
|
||||||
"samlMapping": data.get("samlMapping", existing.get("samlMapping")),
|
|
||||||
}
|
|
||||||
db.idpAttributes.update_one({"_id": attr_id}, {"$set": update_data})
|
|
||||||
return db.idpAttributes.find_one({"_id": attr_id})
|
|
||||||
|
|
||||||
|
|
||||||
def get_attributes(user, id):
|
|
||||||
id = ObjectId(id)
|
|
||||||
db = database.get_db()
|
|
||||||
idp = db.idps.find_one(id)
|
|
||||||
if not idp:
|
|
||||||
return errors.NotFound("IDP not found")
|
|
||||||
if not can_manage_idp(user, idp):
|
|
||||||
raise errors.Forbidden("You can't manage this IdP")
|
|
||||||
|
|
||||||
attributes = list(db.idpAttributes.find({"idp": id}))
|
|
||||||
return {"attributes": attributes}
|
|
||||||
|
|
||||||
|
|
||||||
def delete_attribute(user, id, attr_id):
|
|
||||||
id = ObjectId(id)
|
|
||||||
attr_id = ObjectId(attr_id)
|
|
||||||
db = database.get_db()
|
|
||||||
existing = db.idpAttributes.find_one(attr_id)
|
|
||||||
if not existing:
|
|
||||||
return errors.NotFound("Attribute not found")
|
|
||||||
idp = db.idps.find_one(existing["idp"])
|
|
||||||
if not idp:
|
|
||||||
return errors.NotFound("IDP not found")
|
|
||||||
if not can_manage_idp(user, idp):
|
|
||||||
raise errors.Forbidden("You can't update this IdP")
|
|
||||||
|
|
||||||
db.idpAttributes.delete_one({"_id": attr_id})
|
|
||||||
return {"deletedAttribute": attr_id}
|
|
||||||
|
|
||||||
|
|
||||||
def get_logs(user, id):
|
|
||||||
id = ObjectId(id)
|
|
||||||
db = database.get_db()
|
|
||||||
idp = db.idps.find_one(id)
|
|
||||||
if not idp:
|
|
||||||
return errors.NotFound("IDP not found")
|
|
||||||
if not can_manage_idp(user, idp):
|
|
||||||
raise errors.Forbidden("You can't access this IdP")
|
|
||||||
logs = list(
|
|
||||||
db.requests.find({"idp": id}).sort("createdAt", pymongo.DESCENDING).limit(30)
|
|
||||||
)
|
|
||||||
sps = list(db.idpSps.find({"idp": id}, {"name": 1}))
|
|
||||||
for log in logs:
|
|
||||||
if log.get("data", {}).get("assertion", {}).get("key"):
|
|
||||||
log["data"]["assertion"]["key"] = "REDACTED"
|
|
||||||
for sp in sps:
|
|
||||||
if log["sp"] == sp["_id"]:
|
|
||||||
log["spName"] = sp["name"]
|
|
||||||
break
|
|
||||||
return {"logs": logs}
|
|
||||||
|
|
||||||
|
|
||||||
def get_oauth_logs(user, id):
|
|
||||||
id = ObjectId(id)
|
|
||||||
db = database.get_db()
|
|
||||||
idp = db.idps.find_one(id)
|
|
||||||
if not idp:
|
|
||||||
return errors.NotFound("IDP not found")
|
|
||||||
if not can_manage_idp(user, idp):
|
|
||||||
raise errors.Forbidden("You can't access this IdP")
|
|
||||||
logs = list(
|
|
||||||
db.oauthRequests.find({"idp": id})
|
|
||||||
.sort("createdAt", pymongo.DESCENDING)
|
|
||||||
.limit(30)
|
|
||||||
)
|
|
||||||
sps = list(db.idpSps.find({"idp": id}, {"name": 1}))
|
|
||||||
for log in logs:
|
|
||||||
for sp in sps:
|
|
||||||
if log["sp"] == sp["_id"]:
|
|
||||||
log["spName"] = sp["name"]
|
|
||||||
break
|
|
||||||
return {"logs": logs}
|
|
@ -1,33 +0,0 @@
|
|||||||
from util import database, errors, util
|
|
||||||
|
|
||||||
|
|
||||||
def get_users(user):
|
|
||||||
if not util.has_permission(user, "root"):
|
|
||||||
raise errors.Forbidden("Not allowed")
|
|
||||||
db = database.get_db()
|
|
||||||
users = list(
|
|
||||||
db.users.find(
|
|
||||||
{},
|
|
||||||
{
|
|
||||||
"firstName": 1,
|
|
||||||
"lastName": 1,
|
|
||||||
"email": 1,
|
|
||||||
"createdAt": 1,
|
|
||||||
"lastSeenAt": 1,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.sort("lastSeenAt", -1)
|
|
||||||
.limit(50)
|
|
||||||
)
|
|
||||||
idps = list(
|
|
||||||
db.idps.find(
|
|
||||||
{"user": {"$in": list(map(lambda u: u["_id"], users))}},
|
|
||||||
{"user": 1, "code": 1, "name": 1, "createdAt": 1},
|
|
||||||
)
|
|
||||||
)
|
|
||||||
for u in users:
|
|
||||||
u["idps"] = []
|
|
||||||
for i in idps:
|
|
||||||
if i["user"] == u["_id"]:
|
|
||||||
u["idps"].append(i)
|
|
||||||
return {"users": users}
|
|
@ -1,60 +0,0 @@
|
|||||||
from bson.objectid import ObjectId
|
|
||||||
from util import database, errors, mail
|
|
||||||
|
|
||||||
|
|
||||||
def me(user):
|
|
||||||
return {
|
|
||||||
"_id": user["_id"],
|
|
||||||
"firstName": user.get("firstName"),
|
|
||||||
"lastName": user.get("lastName"),
|
|
||||||
"email": user.get("email"),
|
|
||||||
"subscriptions": user.get("subscriptions", []),
|
|
||||||
"permissions": user.get("permissions", {}),
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def get(user, id):
|
|
||||||
db = database.get_db()
|
|
||||||
if str(user["_id"]) != id:
|
|
||||||
raise errors.Forbidden("Not allowed")
|
|
||||||
return db.users.find_one({"_id": ObjectId(id)}, {"firstName": 1, "lastName": 1})
|
|
||||||
|
|
||||||
|
|
||||||
def update(user, id, data):
|
|
||||||
if not data:
|
|
||||||
raise errors.BadRequest("Invalid request")
|
|
||||||
db = database.get_db()
|
|
||||||
if str(user["_id"]) != id:
|
|
||||||
raise errors.Forbidden("Not allowed")
|
|
||||||
update = {}
|
|
||||||
if data.get("email") and data["email"] != user.get("email"):
|
|
||||||
email = data["email"].lower()
|
|
||||||
existing_user = db.users.find_one({"_id": {"$ne": user["_id"]}, "email": email})
|
|
||||||
if existing_user:
|
|
||||||
raise errors.BadRequest("This new email address is already in use")
|
|
||||||
mail_content = "Dear {0},\n\nThis email is to let you know that the email address for your SSO Tools account has been changed to: {1}.\n\nIf this was not you, and/or you believe your account has been compromised, please login as soon as possible and change your account password.".format(
|
|
||||||
user["firstName"], email
|
|
||||||
)
|
|
||||||
mail.send(
|
|
||||||
{
|
|
||||||
"to_user": user,
|
|
||||||
"subject": "SSOTools Email Address Changed",
|
|
||||||
"text": mail_content,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
mail.send(
|
|
||||||
{
|
|
||||||
"to": email,
|
|
||||||
"subject": "SSOTools Email Address Changed",
|
|
||||||
"text": mail_content,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
update["email"] = email
|
|
||||||
if "firstName" in data:
|
|
||||||
update["firstName"] = data["firstName"]
|
|
||||||
if "lastName" in data:
|
|
||||||
update["lastName"] = data["lastName"]
|
|
||||||
if update:
|
|
||||||
db.users.update_one({"_id": ObjectId(id)}, {"$set": update})
|
|
||||||
return get(user, id)
|
|
381
api/app.py
381
api/app.py
@ -1,349 +1,148 @@
|
|||||||
import sentry_sdk
|
|
||||||
from flask import Flask, jsonify, request
|
from flask import Flask, jsonify, request
|
||||||
from flask_cors import CORS
|
from flask_cors import CORS
|
||||||
from flask_limiter import Limiter
|
from flask_limiter import Limiter
|
||||||
import werkzeug
|
import werkzeug
|
||||||
from webargs import fields, validate
|
from chalicelib.util import util
|
||||||
from webargs.flaskparser import use_args
|
from chalicelib.api import accounts, users, idps, root
|
||||||
from util import util
|
|
||||||
from api import accounts, users, idps, root
|
|
||||||
|
|
||||||
sentry_sdk.init(
|
|
||||||
dsn="https://e5b27a6e04405e77b14f51631b2c96f3@o4508066290532352.ingest.de.sentry.io/4508066432876624",
|
|
||||||
traces_sample_rate=1.0,
|
|
||||||
profiles_sample_rate=1.0,
|
|
||||||
)
|
|
||||||
|
|
||||||
app = Flask(__name__)
|
app = Flask(__name__)
|
||||||
CORS(app)
|
CORS(app)
|
||||||
limiter = Limiter(util.limit_by_user, app=app, default_limits=["20 per minute"])
|
limiter = Limiter(app, default_limits=['20 per minute'], key_func=util.limit_by_user)
|
||||||
|
|
||||||
|
|
||||||
@app.errorhandler(werkzeug.exceptions.TooManyRequests)
|
@app.errorhandler(werkzeug.exceptions.TooManyRequests)
|
||||||
def handle_429(e):
|
def handle_429(e):
|
||||||
return jsonify(
|
return jsonify({'message': 'You\'re making too many requests. Please wait for a few minutes before trying again.', 'Allowed limit': e.description}), 429
|
||||||
{
|
|
||||||
"message": "You're making too many requests. Please wait for a few minutes before trying again.",
|
|
||||||
"Allowed limit": e.description,
|
|
||||||
}
|
|
||||||
), 429
|
|
||||||
|
|
||||||
|
|
||||||
@app.errorhandler(werkzeug.exceptions.BadRequest)
|
@app.errorhandler(werkzeug.exceptions.BadRequest)
|
||||||
def handle_bad_request(e):
|
def handle_bad_request(e):
|
||||||
return jsonify({"message": e.description}), 400
|
return jsonify({'message': e.description}), 400
|
||||||
|
|
||||||
|
|
||||||
@app.errorhandler(werkzeug.exceptions.Unauthorized)
|
@app.errorhandler(werkzeug.exceptions.Unauthorized)
|
||||||
def handle_not_authorized(e):
|
def handle_not_authorized(e):
|
||||||
return jsonify({"message": e.description}), 401
|
return jsonify({'message': e.description}), 401
|
||||||
|
|
||||||
|
|
||||||
@app.errorhandler(werkzeug.exceptions.Forbidden)
|
@app.errorhandler(werkzeug.exceptions.Forbidden)
|
||||||
def handle_forbidden(e):
|
def handle_forbidden(e):
|
||||||
return jsonify({"message": e.description}), 403
|
return jsonify({'message': e.description}), 403
|
||||||
|
|
||||||
|
|
||||||
@app.errorhandler(werkzeug.exceptions.NotFound)
|
@app.errorhandler(werkzeug.exceptions.NotFound)
|
||||||
def handle_not_found(e):
|
def handle_not_found(e):
|
||||||
return jsonify({"message": e.description}), 404
|
return jsonify({'message': e.description}), 404
|
||||||
|
|
||||||
|
|
||||||
@app.errorhandler(werkzeug.exceptions.UnprocessableEntity)
|
|
||||||
def handle_unprocessable_entity(e):
|
|
||||||
validation_errors = e.data.get("messages")
|
|
||||||
message = ""
|
|
||||||
|
|
||||||
def build_message(message, d):
|
|
||||||
if not d:
|
|
||||||
return message
|
|
||||||
for key in d:
|
|
||||||
if isinstance(d[key], dict):
|
|
||||||
message += f"""{str(key)}: """
|
|
||||||
return build_message(message, d[key])
|
|
||||||
elif isinstance(d[key], list):
|
|
||||||
message += f"""{str(key)}: {', '.join(d[key])}\n"""
|
|
||||||
return message
|
|
||||||
|
|
||||||
if validation_errors:
|
|
||||||
message = build_message("", validation_errors.get("json"))
|
|
||||||
return jsonify(
|
|
||||||
{
|
|
||||||
"message": message,
|
|
||||||
"validations": validation_errors,
|
|
||||||
}
|
|
||||||
), 422
|
|
||||||
|
|
||||||
|
|
||||||
# ACCOUNTS
|
# ACCOUNTS
|
||||||
|
|
||||||
|
@app.route('/accounts', methods=['POST'])
|
||||||
|
@limiter.limit('5 per minute', key_func=util.limit_by_client, methods=['POST'])
|
||||||
|
def register():
|
||||||
|
return util.jsonify(accounts.create(request.json))
|
||||||
|
|
||||||
@app.route("/accounts", methods=["POST"])
|
@app.route('/accounts/enrol', methods=['POST'])
|
||||||
@limiter.limit("5 per minute", key_func=util.limit_by_client, methods=["POST"])
|
def enrol():
|
||||||
@use_args(
|
return util.jsonify(accounts.enrol(request.json))
|
||||||
{
|
|
||||||
"email": fields.Email(required=True),
|
|
||||||
"firstName": fields.Str(required=True, validate=validate.Length(min=2)),
|
|
||||||
"lastName": fields.Str(required=True, validate=validate.Length(min=2)),
|
|
||||||
"password": fields.Str(required=True, validate=validate.Length(min=8)),
|
|
||||||
"idpsToClaim": fields.List(fields.Str(), missing=[], allow_none=True),
|
|
||||||
}
|
|
||||||
)
|
|
||||||
def register(args):
|
|
||||||
return util.jsonify(accounts.create(args))
|
|
||||||
|
|
||||||
|
@app.route('/accounts/sessions', methods=['POST'])
|
||||||
|
@limiter.limit('5 per minute', key_func=util.limit_by_client, methods=['POST'])
|
||||||
|
def login():
|
||||||
|
return util.jsonify(accounts.login(request.json))
|
||||||
|
|
||||||
@app.route("/accounts/enrol", methods=["POST"])
|
@app.route('/accounts/sessions', methods=['DELETE'])
|
||||||
@limiter.limit("5 per minute", key_func=util.limit_by_client, methods=["POST"])
|
|
||||||
@use_args({"token": fields.Str(), "password": fields.Str(required=True)})
|
|
||||||
def enrol(args):
|
|
||||||
return util.jsonify(accounts.enrol(args))
|
|
||||||
|
|
||||||
|
|
||||||
@app.route("/accounts/sessions", methods=["POST"])
|
|
||||||
@limiter.limit("5 per minute", key_func=util.limit_by_client, methods=["POST"])
|
|
||||||
@use_args(
|
|
||||||
{
|
|
||||||
"email": fields.Email(required=True),
|
|
||||||
"password": fields.Str(required=True),
|
|
||||||
"idpsToClaim": fields.List(fields.Str(), missing=[], allow_none=True),
|
|
||||||
}
|
|
||||||
)
|
|
||||||
def login(args):
|
|
||||||
return util.jsonify(accounts.login(args))
|
|
||||||
|
|
||||||
|
|
||||||
@app.route("/accounts/sessions", methods=["DELETE"])
|
|
||||||
def logout():
|
def logout():
|
||||||
return util.jsonify(accounts.logout(util.get_user()))
|
return util.jsonify(accounts.logout(util.get_user()))
|
||||||
|
|
||||||
|
@app.route('/accounts', methods=['DELETE'])
|
||||||
|
def delete_account():
|
||||||
|
body = request.json
|
||||||
|
return util.jsonify(accounts.delete(util.get_user(), body.get('password')))
|
||||||
|
|
||||||
@app.route("/accounts", methods=["DELETE"])
|
@app.route('/accounts/password', methods=['PUT'])
|
||||||
@use_args(
|
@limiter.limit('5 per minute', key_func=util.limit_by_user, methods=['PUT'])
|
||||||
{
|
def password():
|
||||||
"password": fields.Str(required=True),
|
body = request.json
|
||||||
}
|
return util.jsonify(accounts.update_password(util.get_user(required=False), body))
|
||||||
)
|
|
||||||
def delete_account(args):
|
|
||||||
return util.jsonify(accounts.delete(util.get_user(), args))
|
|
||||||
|
|
||||||
|
|
||||||
@app.route("/accounts/password", methods=["PUT"])
|
|
||||||
@limiter.limit("5 per minute", key_func=util.limit_by_user, methods=["PUT"])
|
|
||||||
@use_args(
|
|
||||||
{
|
|
||||||
"newPassword": fields.Str(required=True, validate=validate.Length(min=8)),
|
|
||||||
"currentPassword": fields.Str(alow_none=True),
|
|
||||||
"token": fields.Str(allow_none=True),
|
|
||||||
}
|
|
||||||
)
|
|
||||||
def password(args):
|
|
||||||
return util.jsonify(accounts.update_password(util.get_user(required=False), args))
|
|
||||||
|
|
||||||
|
|
||||||
@app.route("/accounts/password/reset", methods=["POST"])
|
|
||||||
@limiter.limit("5 per minute", key_func=util.limit_by_client, methods=["POST"])
|
|
||||||
@use_args(
|
|
||||||
{
|
|
||||||
"email": fields.Email(required=True),
|
|
||||||
}
|
|
||||||
)
|
|
||||||
def reset_password(args):
|
|
||||||
return util.jsonify(accounts.reset_password(args))
|
|
||||||
|
|
||||||
|
@app.route('/accounts/password/reset', methods=['POST'])
|
||||||
|
@limiter.limit('5 per minute', key_func=util.limit_by_client, methods=['POST'])
|
||||||
|
def reset_password():
|
||||||
|
body = request.json
|
||||||
|
return util.jsonify(accounts.reset_password(body))
|
||||||
|
|
||||||
# Users
|
# Users
|
||||||
|
|
||||||
|
@app.route('/users/me', methods=['GET'])
|
||||||
@app.route("/users/me", methods=["GET"])
|
|
||||||
def users_me():
|
def users_me():
|
||||||
return util.jsonify(users.me(util.get_user()))
|
return util.jsonify(users.me(util.get_user()))
|
||||||
|
|
||||||
|
|
||||||
@app.route("/users/<id>", methods=["PUT"])
|
|
||||||
@use_args(
|
|
||||||
{
|
|
||||||
"firstName": fields.Str(validate=validate.Length(min=2)),
|
|
||||||
"lastName": fields.Str(validate=validate.Length(min=2)),
|
|
||||||
"email": fields.Email(),
|
|
||||||
}
|
|
||||||
)
|
|
||||||
def user_route(args, id):
|
|
||||||
return util.jsonify(users.update(util.get_user(), id, args))
|
|
||||||
|
|
||||||
|
@app.route('/users/<id>', methods=['PUT'])
|
||||||
|
def user_route(id):
|
||||||
|
return util.jsonify(users.update(util.get_user(), id, request.json))
|
||||||
|
|
||||||
# IDPs
|
# IDPs
|
||||||
|
|
||||||
|
@app.route('/idps', methods=['GET', 'POST'])
|
||||||
|
def idps_route():
|
||||||
|
if request.method == 'POST':
|
||||||
|
return util.jsonify(idps.create(util.get_user(required=False), request.json))
|
||||||
|
if request.method == 'GET':
|
||||||
|
params = request.args or {}
|
||||||
|
return util.jsonify(idps.get(util.get_user(required=False), params.get('include')))
|
||||||
|
|
||||||
@app.route("/idps", methods=["GET"])
|
@app.route('/idps/<id>', methods=['GET', 'PUT', 'DELETE'])
|
||||||
@use_args({"include": fields.Str(missing="")}, location="query")
|
|
||||||
def idps_route_get(args):
|
|
||||||
return util.jsonify(idps.get(util.get_user(required=False), args.get("include")))
|
|
||||||
|
|
||||||
|
|
||||||
@app.route("/idps", methods=["POST"])
|
|
||||||
@use_args(
|
|
||||||
{
|
|
||||||
"code": fields.Str(required=True, validate=validate.Length(min=2)),
|
|
||||||
"name": fields.Str(required=True, validate=validate.Length(min=2)),
|
|
||||||
}
|
|
||||||
)
|
|
||||||
def idps_route_post(args):
|
|
||||||
return util.jsonify(idps.create(util.get_user(required=False), args))
|
|
||||||
|
|
||||||
|
|
||||||
@app.route("/idps/<id>", methods=["GET", "DELETE"])
|
|
||||||
def idp_route(id):
|
def idp_route(id):
|
||||||
if request.method == "GET":
|
if request.method == 'GET':
|
||||||
return util.jsonify(idps.get_one(util.get_user(required=False), id))
|
return util.jsonify(idps.get_one(util.get_user(required=False), id))
|
||||||
if request.method == "DELETE":
|
if request.method == 'PUT':
|
||||||
return util.jsonify(idps.delete(util.get_user(required=False), id))
|
return util.jsonify(idps.update(util.get_user(required=False), id, request.json))
|
||||||
|
if request.method == 'DELETE':
|
||||||
|
return util.jsonify(idps.delete(util.get_user(required=False), id))
|
||||||
|
|
||||||
|
@app.route('/idps/<id>/sps', methods=['GET', 'POST'])
|
||||||
@app.route("/idps/<id>", methods=["PUT"])
|
def idp_sps_route(id):
|
||||||
@use_args(
|
if request.method == 'GET':
|
||||||
{
|
|
||||||
"code": fields.Str(validate=validate.Length(min=2)),
|
|
||||||
"name": fields.Str(validate=validate.Length(min=2)),
|
|
||||||
}
|
|
||||||
)
|
|
||||||
def idp_route_put(args, id):
|
|
||||||
return util.jsonify(idps.update(util.get_user(required=False), id, args))
|
|
||||||
|
|
||||||
|
|
||||||
@app.route("/idps/<id>/sps", methods=["GET"])
|
|
||||||
def idp_sps_route_get(id):
|
|
||||||
return util.jsonify(idps.get_sps(util.get_user(required=False), id))
|
return util.jsonify(idps.get_sps(util.get_user(required=False), id))
|
||||||
|
if request.method == 'POST':
|
||||||
|
return util.jsonify(idps.create_sp(util.get_user(required=False), id, request.json))
|
||||||
|
|
||||||
|
@app.route('/idps/<id>/sps/<sp_id>', methods=['PUT', 'DELETE'])
|
||||||
@app.route("/idps/<id>/sps", methods=["POST"])
|
|
||||||
@use_args(
|
|
||||||
{
|
|
||||||
"name": fields.Str(required=True, validate=validate.Length(min=2)),
|
|
||||||
"entityId": fields.Str(),
|
|
||||||
"serviceUrl": fields.Str(),
|
|
||||||
"callbackUrl": fields.Str(),
|
|
||||||
"logoutUrl": fields.Str(),
|
|
||||||
"logoutCallbackUrl": fields.Str(),
|
|
||||||
"oauth2RedirectUri": fields.Str(),
|
|
||||||
}
|
|
||||||
)
|
|
||||||
def idp_sps_route_post(args, id):
|
|
||||||
return util.jsonify(idps.create_sp(util.get_user(required=False), id, args))
|
|
||||||
|
|
||||||
|
|
||||||
@app.route("/idps/<id>/sps/<sp_id>", methods=["PUT"])
|
|
||||||
@use_args(
|
|
||||||
{
|
|
||||||
"name": fields.Str(required=True, validate=validate.Length(min=2)),
|
|
||||||
"entityId": fields.Str(),
|
|
||||||
"serviceUrl": fields.Str(),
|
|
||||||
"callbackUrl": fields.Str(),
|
|
||||||
"logoutUrl": fields.Str(),
|
|
||||||
"logoutCallbackUrl": fields.Str(),
|
|
||||||
"oauth2RedirectUri": fields.Str(),
|
|
||||||
}
|
|
||||||
)
|
|
||||||
def idp_sp_route_put(args, id, sp_id):
|
|
||||||
return util.jsonify(idps.update_sp(util.get_user(required=False), id, sp_id, args))
|
|
||||||
|
|
||||||
|
|
||||||
@app.route("/idps/<id>/sps/<sp_id>", methods=["DELETE"])
|
|
||||||
def idp_sp_route(id, sp_id):
|
def idp_sp_route(id, sp_id):
|
||||||
|
if request.method == 'DELETE':
|
||||||
return util.jsonify(idps.delete_sp(util.get_user(required=False), id, sp_id))
|
return util.jsonify(idps.delete_sp(util.get_user(required=False), id, sp_id))
|
||||||
|
if request.method == 'PUT':
|
||||||
|
return util.jsonify(idps.update_sp(util.get_user(required=False), id, sp_id, request.json))
|
||||||
|
|
||||||
|
@app.route('/idps/<id>/users', methods=['GET', 'POST'])
|
||||||
@app.route("/idps/<id>/users", methods=["GET"])
|
def idp_users_route(id):
|
||||||
def idp_users_route_get(id):
|
if request.method == 'GET':
|
||||||
return util.jsonify(idps.get_users(util.get_user(required=False), id))
|
return util.jsonify(idps.get_users(util.get_user(required=False), id))
|
||||||
|
if request.method == 'POST':
|
||||||
|
return util.jsonify(idps.create_user(util.get_user(required=False), id, request.json))
|
||||||
|
|
||||||
|
@app.route('/idps/<id>/users/<user_id>', methods=['PUT', 'DELETE'])
|
||||||
@app.route("/idps/<id>/users", methods=["POST"])
|
def idp_user_route(id, user_id):
|
||||||
@use_args(
|
if request.method == 'DELETE':
|
||||||
{
|
|
||||||
"email": fields.Email(required=True),
|
|
||||||
"firstName": fields.Str(),
|
|
||||||
"lastName": fields.Str(),
|
|
||||||
"password": fields.Str(required=True, validate=validate.Length(min=3)),
|
|
||||||
"attributes": fields.Dict(),
|
|
||||||
}
|
|
||||||
)
|
|
||||||
def idp_users_route_post(args, id):
|
|
||||||
return util.jsonify(idps.create_user(util.get_user(required=False), id, args))
|
|
||||||
|
|
||||||
|
|
||||||
@app.route("/idps/<id>/users/<user_id>", methods=["PUT"])
|
|
||||||
@use_args(
|
|
||||||
{
|
|
||||||
"email": fields.Email(required=True),
|
|
||||||
"firstName": fields.Str(),
|
|
||||||
"lastName": fields.Str(),
|
|
||||||
"password": fields.Str(),
|
|
||||||
"attributes": fields.Dict(),
|
|
||||||
}
|
|
||||||
)
|
|
||||||
def idp_user_route_put(args, id, user_id):
|
|
||||||
return util.jsonify(
|
|
||||||
idps.update_user(util.get_user(required=False), id, user_id, args)
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@app.route("/idps/<id>/users/<user_id>", methods=["DELETE"])
|
|
||||||
def idp_user_route_delete(id, user_id):
|
|
||||||
return util.jsonify(idps.delete_user(util.get_user(required=False), id, user_id))
|
return util.jsonify(idps.delete_user(util.get_user(required=False), id, user_id))
|
||||||
|
if request.method == 'PUT':
|
||||||
|
return util.jsonify(idps.update_user(util.get_user(required=False), id, user_id, request.json))
|
||||||
|
|
||||||
|
@app.route('/idps/<id>/attributes', methods=['GET', 'POST'])
|
||||||
@app.route("/idps/<id>/attributes", methods=["GET"])
|
def idp_attributes_route(id):
|
||||||
def idp_attributes_route_get(id):
|
if request.method == 'GET':
|
||||||
return util.jsonify(idps.get_attributes(util.get_user(required=False), id))
|
return util.jsonify(idps.get_attributes(util.get_user(required=False), id))
|
||||||
|
if request.method == 'POST':
|
||||||
|
return util.jsonify(idps.create_attribute(util.get_user(required=False), id, request.json))
|
||||||
|
|
||||||
|
@app.route('/idps/<id>/attributes/<attr_id>', methods=['PUT', 'DELETE'])
|
||||||
|
def idp_attribute_route(id, attr_id):
|
||||||
|
if request.method == 'DELETE':
|
||||||
|
return util.jsonify(idps.delete_attribute(util.get_user(required=False), id, attr_id))
|
||||||
|
if request.method == 'PUT':
|
||||||
|
return util.jsonify(idps.update_attribute(util.get_user(required=False), id, attr_id, request.json))
|
||||||
|
|
||||||
@app.route("/idps/<id>/attributes", methods=["POST"])
|
@app.route('/idps/<id>/saml2/logs', methods=['GET'])
|
||||||
@use_args(
|
|
||||||
{
|
|
||||||
"name": fields.Str(required=True, validate=validate.Length(min=2)),
|
|
||||||
"defaultValue": fields.Str(),
|
|
||||||
"samlMapping": fields.Str(),
|
|
||||||
}
|
|
||||||
)
|
|
||||||
def idp_attributes_route_post(args, id):
|
|
||||||
return util.jsonify(idps.create_attribute(util.get_user(required=False), id, args))
|
|
||||||
|
|
||||||
|
|
||||||
@app.route("/idps/<id>/attributes/<attr_id>", methods=["PUT"])
|
|
||||||
@use_args(
|
|
||||||
{
|
|
||||||
"name": fields.Str(required=True, validate=validate.Length(min=2)),
|
|
||||||
"defaultValue": fields.Str(),
|
|
||||||
"samlMapping": fields.Str(),
|
|
||||||
}
|
|
||||||
)
|
|
||||||
def idp_attribute_route_put(args, id, attr_id):
|
|
||||||
return util.jsonify(
|
|
||||||
idps.update_attribute(util.get_user(required=False), id, attr_id, args)
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@app.route("/idps/<id>/attributes/<attr_id>", methods=["DELETE"])
|
|
||||||
def idp_attribute_route_delete(id, attr_id):
|
|
||||||
return util.jsonify(
|
|
||||||
idps.delete_attribute(util.get_user(required=False), id, attr_id)
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@app.route("/idps/<id>/saml2/logs", methods=["GET"])
|
|
||||||
def idp_saml_logs_route(id):
|
def idp_saml_logs_route(id):
|
||||||
return util.jsonify(idps.get_logs(util.get_user(required=False), id))
|
return util.jsonify(idps.get_logs(util.get_user(required=False), id))
|
||||||
|
|
||||||
|
@app.route('/idps/<id>/oauth2/logs', methods=['GET'])
|
||||||
@app.route("/idps/<id>/oauth2/logs", methods=["GET"])
|
|
||||||
def idp_oauth_logs_route(id):
|
def idp_oauth_logs_route(id):
|
||||||
return util.jsonify(idps.get_oauth_logs(util.get_user(required=False), id))
|
return util.jsonify(idps.get_oauth_logs(util.get_user(required=False), id))
|
||||||
|
|
||||||
|
|
||||||
# Root
|
# Root
|
||||||
|
|
||||||
|
@app.route('/root/users', methods=['GET'])
|
||||||
@app.route("/root/users", methods=["GET"])
|
|
||||||
def root_users():
|
def root_users():
|
||||||
return util.jsonify(root.get_users(util.get_user()))
|
return util.jsonify(root.get_users(util.get_user()))
|
0
api/chalicelib/api/__init__.py
Normal file
0
api/chalicelib/api/__init__.py
Normal file
162
api/chalicelib/api/accounts.py
Normal file
162
api/chalicelib/api/accounts.py
Normal file
@ -0,0 +1,162 @@
|
|||||||
|
import datetime, jwt, bcrypt, os
|
||||||
|
from bson.objectid import ObjectId
|
||||||
|
from chalicelib.util import database, mail, errors
|
||||||
|
|
||||||
|
jwt_secret = os.environ.get('JWT_SECRET')
|
||||||
|
|
||||||
|
def create(data):
|
||||||
|
email = data.get('email')
|
||||||
|
first_name = data.get('firstName')
|
||||||
|
last_name = data.get('lastName')
|
||||||
|
password = data.get('password')
|
||||||
|
if not email or len(email) < 6: raise errors.BadRequest('Your name or email is too short or invalid.')
|
||||||
|
if not password or len(password) < 8: raise errors.BadRequest('Your password should be at least 8 characters.')
|
||||||
|
email = email.lower()
|
||||||
|
idps_to_claim = data.get('idpsToClaim', []) or []
|
||||||
|
idps_to_claim = list(map(lambda i: ObjectId(i), idps_to_claim))
|
||||||
|
db = database.get_db()
|
||||||
|
existingUser = db.users.find_one({'email': email})
|
||||||
|
if existingUser: raise errors.BadRequest('An account with this email already exists.')
|
||||||
|
|
||||||
|
hashed_password = bcrypt.hashpw(password.encode("utf-8"), bcrypt.gensalt())
|
||||||
|
new_user = {
|
||||||
|
'firstName': first_name,
|
||||||
|
'lastName': last_name,
|
||||||
|
'email': email,
|
||||||
|
'password': hashed_password,
|
||||||
|
'createdAt': datetime.datetime.utcnow()
|
||||||
|
}
|
||||||
|
result = db.users.insert_one(new_user)
|
||||||
|
new_user['_id'] = result.inserted_id
|
||||||
|
if len(idps_to_claim):
|
||||||
|
db.idps.update({'_id': {'$in': idps_to_claim}, 'user': {'$exists': False}}, {'$set': {'user': new_user['_id']}}, multi=True)
|
||||||
|
mail.send({
|
||||||
|
'to': os.environ.get('ADMIN_EMAIL'),
|
||||||
|
'subject': '{} signup'.format(os.environ.get('APP_NAME')),
|
||||||
|
'text': 'A new user signed up with email {0}'.format(email)
|
||||||
|
})
|
||||||
|
return {'token': generate_access_token(new_user['_id'])}
|
||||||
|
|
||||||
|
def enrol(data):
|
||||||
|
if not data or 'token' not in data or 'password' not in data: raise errors.BadRequest('Invalid request')
|
||||||
|
token = data.get('token')
|
||||||
|
password = data.get('password')
|
||||||
|
if not token: raise errors.BadRequest('Invalid token')
|
||||||
|
if not password or len(password) < 8: raise errors.BadRequest('Your password should be at least 8 characters.')
|
||||||
|
try:
|
||||||
|
db = database.get_db()
|
||||||
|
id = jwt.decode(data['token'], jwt_secret)['sub']
|
||||||
|
user = db.users.find_one({'_id': ObjectId(id), 'tokens.enrolment': token})
|
||||||
|
hashed_password = bcrypt.hashpw(password.encode("utf-8"), bcrypt.gensalt())
|
||||||
|
db.users.update({'_id': user['_id']}, {'$set': {'password': hashed_password}, '$unset': {'tokens.enrolment': ''}})
|
||||||
|
return {'token': generate_access_token(user['_id'])}
|
||||||
|
except Exception as e:
|
||||||
|
print(e)
|
||||||
|
raise errors.BadRequest('Unable to enrol your account. Your token may be invalid or expired.')
|
||||||
|
|
||||||
|
def login(data):
|
||||||
|
email = data.get('email')
|
||||||
|
password = data.get('password')
|
||||||
|
idps_to_claim = data.get('idpsToClaim', []) or []
|
||||||
|
idps_to_claim = list(map(lambda i: ObjectId(i), idps_to_claim))
|
||||||
|
|
||||||
|
db = database.get_db()
|
||||||
|
user = db.users.find_one({'email': email.lower()})
|
||||||
|
try:
|
||||||
|
if user and bcrypt.checkpw(password.encode("utf-8"), user['password']):
|
||||||
|
if len(idps_to_claim):
|
||||||
|
db.idps.update({'_id': {'$in': idps_to_claim}, 'user': {'$exists': False}}, {'$set': {'user': user['_id']}}, multi=True)
|
||||||
|
return {'token': generate_access_token(user['_id'])}
|
||||||
|
else:
|
||||||
|
raise errors.BadRequest('Your email or password is incorrect.')
|
||||||
|
except Exception as e:
|
||||||
|
print(e)
|
||||||
|
raise errors.BadRequest('Your email or password is incorrect.')
|
||||||
|
|
||||||
|
def logout(user):
|
||||||
|
db = database.get_db()
|
||||||
|
db.users.update({'_id': user['_id']}, {'$pull': {'tokens.login': user['currentToken']}})
|
||||||
|
return {'loggedOut': True}
|
||||||
|
|
||||||
|
def update_password(user, data):
|
||||||
|
if not data: raise errors.BadRequest('Invalid request')
|
||||||
|
if 'newPassword' not in data: raise errors.BadRequest('Invalid request')
|
||||||
|
if len(data['newPassword']) < 8: raise errors.BadRequest('New password is too short')
|
||||||
|
|
||||||
|
db = database.get_db()
|
||||||
|
if 'currentPassword' in data:
|
||||||
|
if not bcrypt.checkpw(data['currentPassword'].encode('utf-8'), user['password']):
|
||||||
|
raise errors.BadRequest('Incorrect password')
|
||||||
|
elif 'token' in data:
|
||||||
|
try:
|
||||||
|
id = jwt.decode(data['token'], jwt_secret, algorithms=['HS256'])['sub']
|
||||||
|
user = db.users.find_one({'_id': ObjectId(id), 'tokens.passwordReset': data['token']})
|
||||||
|
if not user: raise Exception
|
||||||
|
except Exception as e:
|
||||||
|
print(e)
|
||||||
|
raise errors.BadRequest('There was a problem updating your password. Your token may be invalid or out of date')
|
||||||
|
else:
|
||||||
|
raise errors.BadRequest('Current password or reset token is required')
|
||||||
|
if not user: raise errors.BadRequest('Unable to change your password')
|
||||||
|
|
||||||
|
hashed_password = bcrypt.hashpw(data['newPassword'].encode("utf-8"), bcrypt.gensalt())
|
||||||
|
db.users.update({'_id': user['_id']}, {'$set': {'password': hashed_password}, '$unset': {'tokens.passwordReset': ''}})
|
||||||
|
return {'passwordUpdated': True}
|
||||||
|
|
||||||
|
def delete(user, password):
|
||||||
|
if not password or not bcrypt.checkpw(password.encode('utf-8'), user['password']):
|
||||||
|
raise errors.BadRequest('Incorrect password')
|
||||||
|
db = database.get_db()
|
||||||
|
for idp in db.idps.find({'user': user['_id']}):
|
||||||
|
db.idpSps.remove({'idp': idp['_id']})
|
||||||
|
db.idpUsers.remove({'idp': idp['_id']})
|
||||||
|
db.idpAttributes.remove({'idp': idp['_id']})
|
||||||
|
db.idps.remove({'user': user['_id']})
|
||||||
|
db.users.remove({'_id': user['_id']})
|
||||||
|
return {'deletedUser': user['_id']}
|
||||||
|
|
||||||
|
def generate_access_token(user_id):
|
||||||
|
payload = {
|
||||||
|
'exp': datetime.datetime.utcnow() + datetime.timedelta(days=30),
|
||||||
|
'iat': datetime.datetime.utcnow(),
|
||||||
|
'sub': str(user_id)
|
||||||
|
}
|
||||||
|
token = jwt.encode(payload, jwt_secret, algorithm='HS256')
|
||||||
|
db = database.get_db()
|
||||||
|
db.users.update({'_id': user_id}, {'$addToSet': {'tokens.login': token}})
|
||||||
|
return token
|
||||||
|
|
||||||
|
def get_user_context(token):
|
||||||
|
if not token: return None
|
||||||
|
try:
|
||||||
|
payload = jwt.decode(token, jwt_secret, algorithms=['HS256'])
|
||||||
|
id = payload['sub']
|
||||||
|
if id:
|
||||||
|
db = database.get_db()
|
||||||
|
user = db.users.find_one({'_id': ObjectId(id), 'tokens.login': token})
|
||||||
|
db.users.update({'_id': user['_id']}, {'$set': {'lastSeenAt': datetime.datetime.now()}})
|
||||||
|
user['currentToken'] = token
|
||||||
|
return user
|
||||||
|
except Exception as e:
|
||||||
|
print(e)
|
||||||
|
return None
|
||||||
|
|
||||||
|
def reset_password(data):
|
||||||
|
if not data or not 'email' in data: raise errors.BadRequest('Invalid request')
|
||||||
|
if len(data['email']) < 5: raise errors.BadRequest('Your email is too short')
|
||||||
|
db = database.get_db()
|
||||||
|
user = db.users.find_one({'email': data['email'].lower()})
|
||||||
|
if user:
|
||||||
|
payload = {
|
||||||
|
'exp': datetime.datetime.utcnow() + datetime.timedelta(days=1),
|
||||||
|
'iat': datetime.datetime.utcnow(),
|
||||||
|
'sub': str(user['_id'])
|
||||||
|
}
|
||||||
|
token = jwt.encode(payload, jwt_secret, algorithm='HS256')
|
||||||
|
mail.send({
|
||||||
|
'to_user': user,
|
||||||
|
'subject': 'Reset your password',
|
||||||
|
'text': 'Dear {0},\n\nA password reset email was recently requested for your SSO Tools account. If this was you and you want to continue, please follow the link below:\n\n{1}\n\nThis link will expire after 24 hours.\n\nIf this was not you, then someone may be trying to gain access to your account. We recommend using a strong and unique password for your account.'.format(user['firstName'], 'https://sso.tools/password/reset?token=' + token)
|
||||||
|
})
|
||||||
|
db.users.update({'_id': user['_id']}, {'$set': {'tokens.passwordReset': token}})
|
||||||
|
return {'passwordResetEmailSent': True}
|
348
api/chalicelib/api/idps.py
Normal file
348
api/chalicelib/api/idps.py
Normal file
@ -0,0 +1,348 @@
|
|||||||
|
from uuid import uuid4
|
||||||
|
import bcrypt, re, random, string
|
||||||
|
from OpenSSL import crypto
|
||||||
|
import pymongo
|
||||||
|
from bson.objectid import ObjectId
|
||||||
|
from chalicelib.util import database, errors, util
|
||||||
|
|
||||||
|
forbidden_codes = ['', 'app', 'my', 'www', 'support', 'mail', 'email', 'dashboard', 'ssotools', 'myidp']
|
||||||
|
|
||||||
|
def create_self_signed_cert(name):
|
||||||
|
# create a key pair
|
||||||
|
k = crypto.PKey()
|
||||||
|
k.generate_key(crypto.TYPE_RSA, 1024)
|
||||||
|
|
||||||
|
# create a self-signed cert
|
||||||
|
cert = crypto.X509()
|
||||||
|
cert.get_subject().C = "GB"
|
||||||
|
cert.get_subject().O = "SSO Tools"
|
||||||
|
cert.get_subject().OU = name
|
||||||
|
cert.gmtime_adj_notBefore(0)
|
||||||
|
cert.gmtime_adj_notAfter(20*365*24*60*60)
|
||||||
|
cert.set_issuer(cert.get_subject())
|
||||||
|
cert.set_pubkey(k)
|
||||||
|
cert.sign(k, 'sha256')
|
||||||
|
dumped = crypto.dump_certificate(crypto.FILETYPE_PEM, cert)
|
||||||
|
dumped_key = crypto.dump_privatekey(crypto.FILETYPE_PEM, k)
|
||||||
|
return {'cert': dumped, 'key': dumped_key}
|
||||||
|
|
||||||
|
def can_manage_idp(user, idp):
|
||||||
|
if not idp: return False
|
||||||
|
if not user: return not idp.get('user')
|
||||||
|
if user: return util.has_permission(user, 'root') or idp.get('user') == user['_id'] or not idp.get('user')
|
||||||
|
|
||||||
|
def create(user, data):
|
||||||
|
db = database.get_db()
|
||||||
|
if not data or not data.get('code') or not data.get('name'): return errors.BadRequest('Name and issuer are required')
|
||||||
|
code = data.get('code').lower().strip()
|
||||||
|
|
||||||
|
if not len(code) or code in forbidden_codes or db.idps.find_one({'code': code}):
|
||||||
|
raise errors.BadRequest('The issuer is invalid or is already in use')
|
||||||
|
if not re.match(r'^([a-zA-Z0-9\-_]+)$', code): raise errors.BadRequest('The IdP issuer is invalid. Please just use letters, numbers, hyphens, and underscores.')
|
||||||
|
|
||||||
|
x509 = create_self_signed_cert(data['name'])
|
||||||
|
idp = {
|
||||||
|
'name': data.get('name'),
|
||||||
|
'code': code,
|
||||||
|
'saml': {
|
||||||
|
'certificate': x509['cert'].decode('utf-8'),
|
||||||
|
'privateKey': x509['key'].decode('utf-8')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if user: idp['user'] = user['_id']
|
||||||
|
result = db.idps.insert_one(idp)
|
||||||
|
idp['_id'] = result.inserted_id
|
||||||
|
|
||||||
|
create_user(user, idp['_id'], {'email': 'joe@example.com', 'firstName': 'Joe', 'lastName': 'Bloggs', 'password': 'password'})
|
||||||
|
create_user(user, idp['_id'], {'email': 'jane@example.com', 'firstName': 'Jane', 'lastName': 'Doe', 'password': 'password'})
|
||||||
|
create_attribute(user, idp['_id'], {'name': 'Group', 'defaultValue': 'staff', 'samlMapping': 'group'})
|
||||||
|
|
||||||
|
return idp
|
||||||
|
|
||||||
|
def get(user, include):
|
||||||
|
db = database.get_db()
|
||||||
|
if include: include = list(map(lambda i: ObjectId(i), include.split(',')))
|
||||||
|
else: include = []
|
||||||
|
|
||||||
|
if user: query = {'$or': [{'user': user['_id']}, {'_id': {'$in': include}, 'user': {'$exists': False}}]}
|
||||||
|
else: query = {'_id': {'$in': include}, 'user': {'$exists': False}}
|
||||||
|
idps = list(db.idps.find(query, {'name': 1, 'code': 1}))
|
||||||
|
return {'idps': idps}
|
||||||
|
|
||||||
|
def get_one(user, id):
|
||||||
|
id = ObjectId(id)
|
||||||
|
db = database.get_db()
|
||||||
|
idp = db.idps.find_one(id)
|
||||||
|
if not idp: raise errors.NotFound('The IdP could not be found')
|
||||||
|
if not can_manage_idp(user, idp): raise errors.Forbidden('You can\'t view this IdP')
|
||||||
|
idp['users'] = list(db.idpUsers.find({'idp': idp['_id']}, {'firstName': 1, 'lastName': 1, 'email': 1}))
|
||||||
|
return idp
|
||||||
|
|
||||||
|
def update(user, id, data):
|
||||||
|
id = ObjectId(id)
|
||||||
|
db = database.get_db()
|
||||||
|
idp = db.idps.find_one(id)
|
||||||
|
if not idp: return errors.NotFound('IDP not found')
|
||||||
|
if not can_manage_idp(user, idp): raise errors.Forbidden('You can\'t edit this IdP')
|
||||||
|
|
||||||
|
if 'code' in data:
|
||||||
|
code = data.get('code').lower().strip()
|
||||||
|
if not len(code) or code in forbidden_codes or db.idps.find_one({'code': code, '_id': {'$ne': id}}):
|
||||||
|
raise errors.BadRequest('This code is invalid or is already in use')
|
||||||
|
if not re.match(r'^([a-zA-Z0-9\-_]+)$', code): raise errors.BadRequest('The IdP issuer is invalid. Please just use letters, numbers, hyphens, and underscores.')
|
||||||
|
else: code = idp['code']
|
||||||
|
update_data = {
|
||||||
|
'name': data.get('name', idp['name']),
|
||||||
|
'code': code
|
||||||
|
}
|
||||||
|
db.idps.update_one({'_id': id}, {'$set': update_data})
|
||||||
|
return get_one(user, id)
|
||||||
|
|
||||||
|
def delete(user, id):
|
||||||
|
id = ObjectId(id)
|
||||||
|
db = database.get_db()
|
||||||
|
idp = db.idps.find_one(id)
|
||||||
|
if not idp: return errors.NotFound('IDP not found')
|
||||||
|
if not can_manage_idp(user, idp): raise errors.Forbidden('You can\'t delete this IdP')
|
||||||
|
db.idps.remove({'_id': id })
|
||||||
|
return {'deletedIDP': id}
|
||||||
|
|
||||||
|
|
||||||
|
#### SPs
|
||||||
|
|
||||||
|
def create_sp(user, id, data):
|
||||||
|
id = ObjectId(id)
|
||||||
|
db = database.get_db()
|
||||||
|
idp = db.idps.find_one(id)
|
||||||
|
if not idp: return errors.NotFound('IDP not found')
|
||||||
|
if not can_manage_idp(user, idp): raise errors.Forbidden('You can\'t manage this IdP')
|
||||||
|
|
||||||
|
sp = {
|
||||||
|
'name': data.get('name'),
|
||||||
|
'type': 'saml',
|
||||||
|
'entityId': data.get('entityId'),
|
||||||
|
'serviceUrl': data.get('serviceUrl'),
|
||||||
|
'callbackUrl': data.get('callbackUrl'),
|
||||||
|
'logoutUrl': data.get('logoutUrl'),
|
||||||
|
'logoutCallbackUrl': data.get('logoutCallbackUrl'),
|
||||||
|
'oauth2ClientId': str(uuid4()),
|
||||||
|
'oauth2ClientSecret': str(''.join(random.choices(string.ascii_uppercase + string.digits, k=32))),
|
||||||
|
'oauth2RedirectUri': data.get('oauth2RedirectUri'),
|
||||||
|
'idp': id
|
||||||
|
}
|
||||||
|
result = db.idpSps.insert_one(sp)
|
||||||
|
sp['_id'] = result.inserted_id
|
||||||
|
return sp
|
||||||
|
|
||||||
|
def update_sp(user, id, sp_id, data):
|
||||||
|
id = ObjectId(id)
|
||||||
|
sp_id = ObjectId(sp_id)
|
||||||
|
db = database.get_db()
|
||||||
|
existing = db.idpSps.find_one(sp_id)
|
||||||
|
if not existing: return errors.NotFound('SP not found')
|
||||||
|
idp = db.idps.find_one(existing['idp'])
|
||||||
|
if not idp: return errors.NotFound('IDP not found')
|
||||||
|
if not can_manage_idp(user, idp): raise errors.Forbidden('You can\'t update this IdP')
|
||||||
|
|
||||||
|
update_data = {
|
||||||
|
'name': data.get('name'),
|
||||||
|
'entityId': data.get('entityId'),
|
||||||
|
'serviceUrl': data.get('serviceUrl'),
|
||||||
|
'callbackUrl': data.get('callbackUrl'),
|
||||||
|
'logoutUrl': data.get('logoutUrl'),
|
||||||
|
'logoutCallbackUrl': data.get('logoutCallbackUrl'),
|
||||||
|
'oauth2RedirectUri': data.get('oauth2RedirectUri'),
|
||||||
|
}
|
||||||
|
db.idpSps.update({'_id': sp_id}, {'$set': update_data})
|
||||||
|
return db.idpSps.find_one({'_id': sp_id}, {'name': 1, 'entityId': 1, 'serviceUrl': 1, 'callbackUrl': 1, 'logoutUrl': 1, 'logoutCallbackUrl': 1})
|
||||||
|
|
||||||
|
def get_sps(user, id):
|
||||||
|
id = ObjectId(id)
|
||||||
|
db = database.get_db()
|
||||||
|
idp = db.idps.find_one(id)
|
||||||
|
if not idp: return errors.NotFound('IDP not found')
|
||||||
|
if not can_manage_idp(user, idp): raise errors.Forbidden('You can\'t update this IdP')
|
||||||
|
|
||||||
|
sps = list(db.idpSps.find({'idp': id}))
|
||||||
|
return {'sps': sps}
|
||||||
|
|
||||||
|
def delete_sp(user, id, sp_id):
|
||||||
|
id = ObjectId(id)
|
||||||
|
sp_id = ObjectId(sp_id)
|
||||||
|
db = database.get_db()
|
||||||
|
|
||||||
|
existing = db.idpSps.find_one(sp_id)
|
||||||
|
if not existing: return errors.NotFound('SP not found')
|
||||||
|
|
||||||
|
idp = db.idps.find_one(existing['idp'])
|
||||||
|
if not idp: return errors.NotFound('IDP not found')
|
||||||
|
if not can_manage_idp(user, idp): raise errors.Forbidden('You can\'t update this IdP')
|
||||||
|
|
||||||
|
db.idpSps.remove({'_id': sp_id})
|
||||||
|
return {'deletedIDPSP': sp_id}
|
||||||
|
|
||||||
|
|
||||||
|
#### Users
|
||||||
|
|
||||||
|
def create_user(user, id, data):
|
||||||
|
id = ObjectId(id)
|
||||||
|
db = database.get_db()
|
||||||
|
idp = db.idps.find_one(id)
|
||||||
|
if not idp: return errors.NotFound('IDP not found')
|
||||||
|
if not can_manage_idp(user, idp): raise errors.Forbidden('You can\'t update this IdP')
|
||||||
|
|
||||||
|
password = data['password']
|
||||||
|
hashed_password = bcrypt.hashpw(password.encode("utf-8"), bcrypt.gensalt())
|
||||||
|
new_user = {
|
||||||
|
'firstName': data.get('firstName'),
|
||||||
|
'lastName': data.get('lastName'),
|
||||||
|
'email': data['email'],
|
||||||
|
'password': hashed_password,
|
||||||
|
'attributes': data.get('attributes', {}),
|
||||||
|
'idp': id
|
||||||
|
}
|
||||||
|
|
||||||
|
result = db.idpUsers.insert_one(new_user)
|
||||||
|
new_user['_id'] = result.inserted_id
|
||||||
|
del new_user['password']
|
||||||
|
return new_user
|
||||||
|
|
||||||
|
def update_user(user, id, user_id, data):
|
||||||
|
id = ObjectId(id)
|
||||||
|
user_id = ObjectId(user_id)
|
||||||
|
db = database.get_db()
|
||||||
|
existing = db.idpUsers.find_one(user_id)
|
||||||
|
if not existing: return errors.NotFound('User not found')
|
||||||
|
idp = db.idps.find_one(existing['idp'])
|
||||||
|
if not idp: return errors.NotFound('IDP not found')
|
||||||
|
if not can_manage_idp(user, idp): raise errors.Forbidden('You can\'t update this IdP')
|
||||||
|
|
||||||
|
update_data = {
|
||||||
|
'firstName': data.get('firstName', existing.get('firstName')),
|
||||||
|
'lastName': data.get('lastName', existing.get('lastName')),
|
||||||
|
'email': data.get('email', existing.get('email')),
|
||||||
|
'attributes': data.get('attributes', existing.get('attributes', {}))
|
||||||
|
}
|
||||||
|
if data.get('password'):
|
||||||
|
hashed_password = bcrypt.hashpw(data['password'].encode("utf-8"), bcrypt.gensalt())
|
||||||
|
update_data['password'] = hashed_password
|
||||||
|
db.idpUsers.update({'_id': user_id}, {'$set': update_data})
|
||||||
|
return db.idpUsers.find_one({'_id': user_id}, {'firstName': 1, 'lastName': 1, 'email': 1, 'attributes': 1})
|
||||||
|
|
||||||
|
def get_users(user, id):
|
||||||
|
id = ObjectId(id)
|
||||||
|
db = database.get_db()
|
||||||
|
idp = db.idps.find_one(id)
|
||||||
|
if not idp: return errors.NotFound('IDP not found')
|
||||||
|
if not can_manage_idp(user, idp): raise errors.Forbidden('You can\'t manage this IdP')
|
||||||
|
|
||||||
|
users = list(db.idpUsers.find({'idp': id}, {'firstName': 1, 'lastName': 1, 'email': 1, 'attributes': 1}))
|
||||||
|
return {'users': users}
|
||||||
|
|
||||||
|
def delete_user(user, id, user_id):
|
||||||
|
id = ObjectId(id)
|
||||||
|
user_id = ObjectId(user_id)
|
||||||
|
db = database.get_db()
|
||||||
|
existing = db.idpUsers.find_one(user_id)
|
||||||
|
if not existing: return errors.NotFound('User not found')
|
||||||
|
idp = db.idps.find_one(existing['idp'])
|
||||||
|
if not idp: return errors.NotFound('IDP not found')
|
||||||
|
if not can_manage_idp(user, idp): raise errors.Forbidden('You can\'t update this IdP')
|
||||||
|
|
||||||
|
db.idpUsers.remove({'_id': user_id})
|
||||||
|
return {'deletedUser': user_id}
|
||||||
|
|
||||||
|
|
||||||
|
#### Attributes
|
||||||
|
|
||||||
|
def create_attribute(user, id, data):
|
||||||
|
id = ObjectId(id)
|
||||||
|
db = database.get_db()
|
||||||
|
idp = db.idps.find_one(id)
|
||||||
|
if not idp: return errors.NotFound('IDP not found')
|
||||||
|
if not can_manage_idp(user, idp): raise errors.Forbidden('You can\'t update this IdP')
|
||||||
|
if not data or 'name' not in data: return errors.BadRequest('Attribute name is required')
|
||||||
|
|
||||||
|
new_attr = {
|
||||||
|
'name': data.get('name'),
|
||||||
|
'defaultValue': data.get('defaultValue'),
|
||||||
|
'samlMapping': data.get('samlMapping'),
|
||||||
|
'idp': id
|
||||||
|
}
|
||||||
|
|
||||||
|
result = db.idpAttributes.insert_one(new_attr)
|
||||||
|
new_attr['_id'] = result.inserted_id
|
||||||
|
return new_attr
|
||||||
|
|
||||||
|
def update_attribute(user, id, attr_id, data):
|
||||||
|
id = ObjectId(id)
|
||||||
|
attr_id = ObjectId(attr_id)
|
||||||
|
db = database.get_db()
|
||||||
|
existing = db.idpAttributes.find_one(attr_id)
|
||||||
|
if not existing: return errors.NotFound('Attribute not found')
|
||||||
|
idp = db.idps.find_one(existing['idp'])
|
||||||
|
if not idp: return errors.NotFound('IDP not found')
|
||||||
|
if not can_manage_idp(user, idp): raise errors.Forbidden('You can\'t update this IdP')
|
||||||
|
|
||||||
|
update_data = {
|
||||||
|
'name': data.get('name', existing.get('name')),
|
||||||
|
'defaultValue': data.get('defaultValue', existing.get('defaultValue')),
|
||||||
|
'samlMapping': data.get('samlMapping', existing.get('samlMapping')),
|
||||||
|
}
|
||||||
|
db.idpAttributes.update({'_id': attr_id}, {'$set': update_data})
|
||||||
|
return db.idpAttributes.find_one({'_id': attr_id})
|
||||||
|
|
||||||
|
def get_attributes(user, id):
|
||||||
|
id = ObjectId(id)
|
||||||
|
db = database.get_db()
|
||||||
|
idp = db.idps.find_one(id)
|
||||||
|
if not idp: return errors.NotFound('IDP not found')
|
||||||
|
if not can_manage_idp(user, idp): raise errors.Forbidden('You can\'t manage this IdP')
|
||||||
|
|
||||||
|
attributes = list(db.idpAttributes.find({'idp': id}))
|
||||||
|
return {'attributes': attributes}
|
||||||
|
|
||||||
|
def delete_attribute(user, id, attr_id):
|
||||||
|
id = ObjectId(id)
|
||||||
|
attr_id = ObjectId(attr_id)
|
||||||
|
db = database.get_db()
|
||||||
|
existing = db.idpAttributes.find_one(attr_id)
|
||||||
|
if not existing: return errors.NotFound('Attribute not found')
|
||||||
|
idp = db.idps.find_one(existing['idp'])
|
||||||
|
if not idp: return errors.NotFound('IDP not found')
|
||||||
|
if not can_manage_idp(user, idp): raise errors.Forbidden('You can\'t update this IdP')
|
||||||
|
|
||||||
|
db.idpAttributes.remove({'_id': attr_id})
|
||||||
|
return {'deletedAttribute': attr_id}
|
||||||
|
|
||||||
|
def get_logs(user, id):
|
||||||
|
id = ObjectId(id)
|
||||||
|
db = database.get_db()
|
||||||
|
idp = db.idps.find_one(id)
|
||||||
|
if not idp: return errors.NotFound('IDP not found')
|
||||||
|
if not can_manage_idp(user, idp): raise errors.Forbidden('You can\'t access this IdP')
|
||||||
|
logs = list(db.requests.find({'idp': id}).sort('createdAt', pymongo.DESCENDING).limit(30))
|
||||||
|
sps = list(db.idpSps.find({'idp': id}, {'name': 1}))
|
||||||
|
for log in logs:
|
||||||
|
if log.get('data', {}).get('assertion', {}).get('key'):
|
||||||
|
log['data']['assertion']['key'] = 'REDACTED'
|
||||||
|
for sp in sps:
|
||||||
|
if log['sp'] == sp['_id']:
|
||||||
|
log['spName'] = sp['name']
|
||||||
|
break
|
||||||
|
return {'logs': logs}
|
||||||
|
|
||||||
|
def get_oauth_logs(user, id):
|
||||||
|
id = ObjectId(id)
|
||||||
|
db = database.get_db()
|
||||||
|
idp = db.idps.find_one(id)
|
||||||
|
if not idp: return errors.NotFound('IDP not found')
|
||||||
|
if not can_manage_idp(user, idp): raise errors.Forbidden('You can\'t access this IdP')
|
||||||
|
logs = list(db.oauthRequests.find({'idp': id}).sort('createdAt', pymongo.DESCENDING).limit(30))
|
||||||
|
sps = list(db.idpSps.find({'idp': id}, {'name': 1}))
|
||||||
|
for log in logs:
|
||||||
|
for sp in sps:
|
||||||
|
if log['sp'] == sp['_id']:
|
||||||
|
log['spName'] = sp['name']
|
||||||
|
break
|
||||||
|
return {'logs': logs}
|
13
api/chalicelib/api/root.py
Normal file
13
api/chalicelib/api/root.py
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import pymongo
|
||||||
|
from chalicelib.util import database, errors, util
|
||||||
|
|
||||||
|
def get_users(user):
|
||||||
|
if not util.has_permission(user, 'root'): raise errors.Forbidden('Not allowed')
|
||||||
|
db = database.get_db()
|
||||||
|
users = list(db.users.find({}, {'firstName': 1, 'lastName': 1, 'email': 1, 'createdAt': 1, 'lastSeenAt': 1}).sort('lastSeenAt', -1).limit(50))
|
||||||
|
idps = list(db.idps.find({'user': {'$in': list(map(lambda u: u['_id'], users))}}, {'user': 1, 'code': 1, 'name': 1, 'createdAt': 1}))
|
||||||
|
for u in users:
|
||||||
|
u['idps'] = []
|
||||||
|
for i in idps:
|
||||||
|
if i['user'] == u['_id']: u['idps'].append(i)
|
||||||
|
return {'users': users}
|
46
api/chalicelib/api/users.py
Normal file
46
api/chalicelib/api/users.py
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
from bson.objectid import ObjectId
|
||||||
|
from chalicelib.util import database, util, errors, mail
|
||||||
|
|
||||||
|
def me(user):
|
||||||
|
db = database.get_db()
|
||||||
|
return {
|
||||||
|
'_id': user['_id'],
|
||||||
|
'firstName': user.get('firstName'),
|
||||||
|
'lastName': user.get('lastName'),
|
||||||
|
'email': user.get('email'),
|
||||||
|
'subscriptions': user.get('subscriptions', []),
|
||||||
|
'permissions': user.get('permissions', {}),
|
||||||
|
}
|
||||||
|
|
||||||
|
def get(user, id):
|
||||||
|
db = database.get_db()
|
||||||
|
if str(user['_id']) != id: raise errors.Forbidden('Not allowed')
|
||||||
|
return db.users.find_one({'_id': ObjectId(id)}, {'firstName': 1, 'lastName': 1})
|
||||||
|
|
||||||
|
def update(user, id, data):
|
||||||
|
if not data: raise errors.BadRequest('Invalid request')
|
||||||
|
db = database.get_db()
|
||||||
|
if str(user['_id']) != id: raise errors.Forbidden('Not allowed')
|
||||||
|
update = {}
|
||||||
|
if data.get('email') and data['email'] != user.get('email'):
|
||||||
|
email = data['email'].lower()
|
||||||
|
existing_user = db.users.find_one({'_id': {'$ne': user['_id']}, 'email': email})
|
||||||
|
if existing_user: raise errors.BadRequest('This new email address is already in use')
|
||||||
|
mail_content = 'Dear {0},\n\nThis email is to let you know that the email address for your SSO Tools account has been changed to: {1}.\n\nIf this was not you, and/or you believe your account has been compromised, please login as soon as possible and change your account password.'.format(user['firstName'], email)
|
||||||
|
mail.send({
|
||||||
|
'to_user': user,
|
||||||
|
'subject': 'SSOTools Email Address Changed',
|
||||||
|
'text': mail_content
|
||||||
|
})
|
||||||
|
mail.send({
|
||||||
|
'to': email,
|
||||||
|
'subject': 'SSOTools Email Address Changed',
|
||||||
|
'text': mail_content
|
||||||
|
})
|
||||||
|
|
||||||
|
update['email'] = email
|
||||||
|
if 'firstName' in data: update['firstName'] = data['firstName']
|
||||||
|
if 'lastName' in data: update['lastName'] = data['lastName']
|
||||||
|
if update:
|
||||||
|
db.users.update({'_id': ObjectId(id)}, {'$set': update})
|
||||||
|
return get(user, id)
|
3
api/chalicelib/util/__init__.py
Normal file
3
api/chalicelib/util/__init__.py
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
import werkzeug
|
||||||
|
|
||||||
|
errors = werkzeug.exceptions
|
10
api/chalicelib/util/database.py
Normal file
10
api/chalicelib/util/database.py
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
import os
|
||||||
|
from pymongo import MongoClient
|
||||||
|
|
||||||
|
db = None
|
||||||
|
|
||||||
|
def get_db():
|
||||||
|
global db
|
||||||
|
if not db:
|
||||||
|
db = MongoClient(os.environ.get('MONGO_URL'))[os.environ.get('MONGO_DATABASE')]
|
||||||
|
return db
|
27
api/chalicelib/util/mail.py
Normal file
27
api/chalicelib/util/mail.py
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
import os
|
||||||
|
import requests
|
||||||
|
|
||||||
|
def send(data):
|
||||||
|
if 'from' not in data:
|
||||||
|
data['from'] = 'SSO Tools <no_reply@mail.sso.tools>'
|
||||||
|
if 'to_user' in data:
|
||||||
|
user = data['to_user']
|
||||||
|
data['to'] = user['firstName'] + ' <' + user['email'] + '>'
|
||||||
|
del data['to_user']
|
||||||
|
data['text'] += '\n\nFrom the team at SSO Tools\n\n\n\n--\n\nReceived this email in error? Please let us know by contacting hello@sso.tools'
|
||||||
|
|
||||||
|
base_url = os.environ.get('MAILGUN_URL')
|
||||||
|
api_key = os.environ.get('MAILGUN_KEY')
|
||||||
|
if base_url and api_key:
|
||||||
|
auth = ('api', api_key)
|
||||||
|
try:
|
||||||
|
response = requests.post(base_url, auth=auth, data=data)
|
||||||
|
response.raise_for_status()
|
||||||
|
except Exception as e:
|
||||||
|
print(e)
|
||||||
|
print('Unable to send email')
|
||||||
|
else:
|
||||||
|
print('Not sending email. Message pasted below.')
|
||||||
|
print(data)
|
||||||
|
|
||||||
|
|
62
api/chalicelib/util/util.py
Normal file
62
api/chalicelib/util/util.py
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
import json, datetime
|
||||||
|
from flask import request
|
||||||
|
from flask_limiter.util import get_remote_address
|
||||||
|
from bson.objectid import ObjectId
|
||||||
|
from chalicelib.api import accounts
|
||||||
|
|
||||||
|
def get_user(required = True):
|
||||||
|
headers = request.headers
|
||||||
|
if not headers.get('Authorization') and required:
|
||||||
|
raise errors.Unauthorized('This resource requires authentication')
|
||||||
|
if headers.get('Authorization'):
|
||||||
|
user = accounts.get_user_context(headers.get('Authorization').replace('Bearer ', ''))
|
||||||
|
if user is None and required:
|
||||||
|
raise errors.Unauthorized('Invalid token')
|
||||||
|
return user
|
||||||
|
return None
|
||||||
|
|
||||||
|
def limit_by_client():
|
||||||
|
data = request.get_json()
|
||||||
|
if data:
|
||||||
|
if data.get('email'): return data.get('email')
|
||||||
|
if data.get('token'): return data.get('token')
|
||||||
|
return get_remote_address()
|
||||||
|
|
||||||
|
def limit_by_user():
|
||||||
|
user = get_user(required = False)
|
||||||
|
return user['_id'] if user else get_remote_address()
|
||||||
|
|
||||||
|
def has_permission(user, permission, scope='global'):
|
||||||
|
if not user or not permission: return False
|
||||||
|
return permission in user.get('permissions', {}).get(scope, [])
|
||||||
|
|
||||||
|
def filter_keys(obj, allowed_keys):
|
||||||
|
filtered = {}
|
||||||
|
for key in allowed_keys:
|
||||||
|
if key in obj:
|
||||||
|
filtered[key] = obj[key]
|
||||||
|
return filtered
|
||||||
|
|
||||||
|
def build_updater(obj, allowed_keys):
|
||||||
|
if not obj: return {}
|
||||||
|
allowed = filter_keys(obj, allowed_keys)
|
||||||
|
updater = {}
|
||||||
|
for key in allowed:
|
||||||
|
if not allowed[key]:
|
||||||
|
if '$unset' not in updater: updater['$unset'] = {}
|
||||||
|
updater['$unset'][key] = ''
|
||||||
|
else:
|
||||||
|
if '$set' not in updater: updater['$set'] = {}
|
||||||
|
updater['$set'][key] = allowed[key]
|
||||||
|
return updater
|
||||||
|
|
||||||
|
class MongoJsonEncoder(json.JSONEncoder):
|
||||||
|
def default(self, obj):
|
||||||
|
if isinstance(obj, (datetime.datetime, datetime.date)):
|
||||||
|
return obj.isoformat()
|
||||||
|
elif isinstance(obj, ObjectId):
|
||||||
|
return str(obj)
|
||||||
|
return json.JSONEncoder.default(self, obj)
|
||||||
|
|
||||||
|
def jsonify(*args, **kwargs):
|
||||||
|
return json.dumps(dict(*args, **kwargs), cls=MongoJsonEncoder)
|
@ -1,4 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
|
|
||||||
ruff format .
|
|
||||||
ruff check --fix .
|
|
1467
api/poetry.lock
generated
1467
api/poetry.lock
generated
File diff suppressed because it is too large
Load Diff
@ -3,29 +3,23 @@ name = "ssotools-api"
|
|||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
description = ""
|
description = ""
|
||||||
authors = ["Will Webberley <will@sso.tools>"]
|
authors = ["Will Webberley <will@sso.tools>"]
|
||||||
package-mode = false
|
|
||||||
|
|
||||||
[tool.poetry.dependencies]
|
[tool.poetry.dependencies]
|
||||||
python = "^3.12"
|
python = "^3.9"
|
||||||
Flask = "^3.0.3"
|
Flask = "^2.1.1"
|
||||||
gunicorn = "^23.0.0"
|
gunicorn = "^20.1.0"
|
||||||
bcrypt = "^4.2.0"
|
bcrypt = "^3.2.0"
|
||||||
dnspython = "^2.6.1"
|
dnspython = "^2.1.0"
|
||||||
PyJWT = "^2.9.0"
|
PyJWT = "^2.0.1"
|
||||||
pymongo = "^4.10.1"
|
pymongo = "^3.11.3"
|
||||||
pyOpenSSL = "^24.2.1"
|
pyOpenSSL = "^20.0.1"
|
||||||
requests = "^2.32.3"
|
requests = "^2.27.1"
|
||||||
Flask-Cors = "^5.0.0"
|
Flask-Cors = "^3.0.10"
|
||||||
Werkzeug = "^3.0.4"
|
Werkzeug = "^2.1.1"
|
||||||
Flask-Limiter = "^3.8.0"
|
Flask-Limiter = "^2.4.5"
|
||||||
sentry-sdk = {extras = ["flask"], version = "^2.15.0"}
|
|
||||||
webargs = "^8.6.0"
|
|
||||||
|
|
||||||
[tool.poetry.dev-dependencies]
|
[tool.poetry.dev-dependencies]
|
||||||
|
|
||||||
[tool.poetry.group.dev.dependencies]
|
|
||||||
ruff = "^0.6.9"
|
|
||||||
|
|
||||||
[build-system]
|
[build-system]
|
||||||
requires = ["poetry-core>=1.0.0"]
|
requires = ["poetry-core>=1.0.0"]
|
||||||
build-backend = "poetry.core.masonry.api"
|
build-backend = "poetry.core.masonry.api"
|
||||||
|
@ -1,3 +0,0 @@
|
|||||||
import werkzeug
|
|
||||||
|
|
||||||
errors = werkzeug.exceptions
|
|
@ -1,11 +0,0 @@
|
|||||||
import os
|
|
||||||
from pymongo import MongoClient
|
|
||||||
|
|
||||||
db = None
|
|
||||||
|
|
||||||
|
|
||||||
def get_db():
|
|
||||||
global db
|
|
||||||
if db is None:
|
|
||||||
db = MongoClient(os.environ.get("MONGO_URL"))[os.environ.get("MONGO_DATABASE")]
|
|
||||||
return db
|
|
@ -1,28 +0,0 @@
|
|||||||
import os
|
|
||||||
import requests
|
|
||||||
|
|
||||||
|
|
||||||
def send(data):
|
|
||||||
if "from" not in data:
|
|
||||||
data["from"] = "SSO Tools <no_reply@mail.sso.tools>"
|
|
||||||
if "to_user" in data:
|
|
||||||
user = data["to_user"]
|
|
||||||
data["to"] = user["firstName"] + " <" + user["email"] + ">"
|
|
||||||
del data["to_user"]
|
|
||||||
data["text"] += (
|
|
||||||
"\n\nFrom the team at SSO Tools\n\n\n\n--\n\nReceived this email in error? Please let us know by contacting hello@sso.tools"
|
|
||||||
)
|
|
||||||
|
|
||||||
base_url = os.environ.get("MAILGUN_URL")
|
|
||||||
api_key = os.environ.get("MAILGUN_KEY")
|
|
||||||
if base_url and api_key:
|
|
||||||
auth = ("api", api_key)
|
|
||||||
try:
|
|
||||||
response = requests.post(base_url, auth=auth, data=data)
|
|
||||||
response.raise_for_status()
|
|
||||||
except Exception as e:
|
|
||||||
print(e)
|
|
||||||
print("Unable to send email")
|
|
||||||
else:
|
|
||||||
print("Not sending email. Message pasted below.")
|
|
||||||
print(data)
|
|
@ -1,80 +0,0 @@
|
|||||||
import json
|
|
||||||
import datetime
|
|
||||||
from flask import request
|
|
||||||
from flask_limiter.util import get_remote_address
|
|
||||||
from bson.objectid import ObjectId
|
|
||||||
from api import accounts
|
|
||||||
from util import errors
|
|
||||||
|
|
||||||
|
|
||||||
def get_user(required=True):
|
|
||||||
headers = request.headers
|
|
||||||
if not headers.get("Authorization") and required:
|
|
||||||
raise errors.Unauthorized("This resource requires authentication")
|
|
||||||
if headers.get("Authorization"):
|
|
||||||
user = accounts.get_user_context(
|
|
||||||
headers.get("Authorization").replace("Bearer ", "")
|
|
||||||
)
|
|
||||||
if user is None and required:
|
|
||||||
raise errors.Unauthorized("Invalid token")
|
|
||||||
return user
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
def limit_by_client():
|
|
||||||
data = request.get_json()
|
|
||||||
if data:
|
|
||||||
if data.get("email"):
|
|
||||||
return data.get("email")
|
|
||||||
if data.get("token"):
|
|
||||||
return data.get("token")
|
|
||||||
return get_remote_address()
|
|
||||||
|
|
||||||
|
|
||||||
def limit_by_user():
|
|
||||||
user = get_user(required=False)
|
|
||||||
return user["_id"] if user else get_remote_address()
|
|
||||||
|
|
||||||
|
|
||||||
def has_permission(user, permission, scope="global"):
|
|
||||||
if not user or not permission:
|
|
||||||
return False
|
|
||||||
return permission in user.get("permissions", {}).get(scope, [])
|
|
||||||
|
|
||||||
|
|
||||||
def filter_keys(obj, allowed_keys):
|
|
||||||
filtered = {}
|
|
||||||
for key in allowed_keys:
|
|
||||||
if key in obj:
|
|
||||||
filtered[key] = obj[key]
|
|
||||||
return filtered
|
|
||||||
|
|
||||||
|
|
||||||
def build_updater(obj, allowed_keys):
|
|
||||||
if not obj:
|
|
||||||
return {}
|
|
||||||
allowed = filter_keys(obj, allowed_keys)
|
|
||||||
updater = {}
|
|
||||||
for key in allowed:
|
|
||||||
if not allowed[key]:
|
|
||||||
if "$unset" not in updater:
|
|
||||||
updater["$unset"] = {}
|
|
||||||
updater["$unset"][key] = ""
|
|
||||||
else:
|
|
||||||
if "$set" not in updater:
|
|
||||||
updater["$set"] = {}
|
|
||||||
updater["$set"][key] = allowed[key]
|
|
||||||
return updater
|
|
||||||
|
|
||||||
|
|
||||||
class MongoJsonEncoder(json.JSONEncoder):
|
|
||||||
def default(self, obj):
|
|
||||||
if isinstance(obj, (datetime.datetime, datetime.date)):
|
|
||||||
return obj.isoformat()
|
|
||||||
elif isinstance(obj, ObjectId):
|
|
||||||
return str(obj)
|
|
||||||
return json.JSONEncoder.default(self, obj)
|
|
||||||
|
|
||||||
|
|
||||||
def jsonify(*args, **kwargs):
|
|
||||||
return json.dumps(dict(*args, **kwargs), cls=MongoJsonEncoder)
|
|
@ -1,4 +1,4 @@
|
|||||||
FROM node:20
|
FROM node:19
|
||||||
|
|
||||||
# Create app directory
|
# Create app directory
|
||||||
WORKDIR /usr/src/app
|
WORKDIR /usr/src/app
|
||||||
|
@ -21,7 +21,7 @@ source envfile
|
|||||||
Then you can run the service:
|
Then you can run the service:
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
yarn start
|
node index
|
||||||
```
|
```
|
||||||
|
|
||||||
**Note:** The service expects to be able to connect to a local MongoDB running on port 27017.
|
**Note:** The service expects to be able to connect to a local MongoDB running on port 27017.
|
||||||
|
@ -1,19 +1,19 @@
|
|||||||
import { MongoClient } from 'mongodb'
|
const { MongoClient } = require('mongodb');
|
||||||
|
|
||||||
const database = {
|
const database = {
|
||||||
db: null,
|
db: null,
|
||||||
connect: async () => {
|
connect: async () => {
|
||||||
const url = process.env.MONGO_URL
|
const url = process.env.MONGO_URL;
|
||||||
const dbName = process.env.MONGO_DATABASE
|
const dbName = process.env.MONGO_DATABASE;
|
||||||
const client = new MongoClient(url)
|
const client = new MongoClient(url);
|
||||||
await client.connect()
|
await client.connect();
|
||||||
database.db = client.db(dbName)
|
database.db = client.db(dbName);
|
||||||
return database.db
|
return database.db;
|
||||||
},
|
},
|
||||||
collection: async (name) => {
|
collection: async (name) => {
|
||||||
const db = database.db || await database.connect()
|
const db = database.db || await database.connect();
|
||||||
return db && db.collection(name)
|
return db && db.collection(name);
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
export default database
|
module.exports = database;
|
||||||
|
412
idp/idp.js
412
idp/idp.js
@ -1,10 +1,9 @@
|
|||||||
import crypto from 'crypto'
|
var crypto = require('crypto');
|
||||||
import zlib from 'zlib'
|
var zlib = require('zlib');
|
||||||
import { Buffer } from 'buffer'
|
var Buffer = require('buffer').Buffer;
|
||||||
import { DOMParser } from 'xmldom'
|
var Parser = require('xmldom').DOMParser;
|
||||||
import { SignedXml } from 'xml-crypto'
|
var SignedXml = require('xml-crypto').SignedXml;
|
||||||
|
var samlp = `<saml:Assertion xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" Version="2.0" ID="" IssueInstant="">
|
||||||
const SAMLP = `<saml:Assertion xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" Version="2.0" ID="" IssueInstant="">
|
|
||||||
<saml:Issuer></saml:Issuer>
|
<saml:Issuer></saml:Issuer>
|
||||||
<saml:Subject>
|
<saml:Subject>
|
||||||
<saml:NameID Format="urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified" />
|
<saml:NameID Format="urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified" />
|
||||||
@ -19,231 +18,224 @@ const SAMLP = `<saml:Assertion xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion
|
|||||||
<saml:AuthnContextClassRef>urn:oasis:names:tc:SAML:2.0:ac:classes:unspecified</saml:AuthnContextClassRef>
|
<saml:AuthnContextClassRef>urn:oasis:names:tc:SAML:2.0:ac:classes:unspecified</saml:AuthnContextClassRef>
|
||||||
</saml:AuthnContext>
|
</saml:AuthnContext>
|
||||||
</saml:AuthnStatement>
|
</saml:AuthnStatement>
|
||||||
</saml:Assertion>`
|
</saml:Assertion>`;
|
||||||
const ASSERTION_NS = 'urn:oasis:names:tc:SAML:2.0:assertion'
|
|
||||||
const SAMLP_NS = 'urn:oasis:names:tc:SAML:2.0:protocol'
|
function pemToCert(pem) {
|
||||||
const ALGORITHMS = {
|
var cert = /-----BEGIN CERTIFICATE-----([^-]*)-----END CERTIFICATE-----/g.exec(pem.toString());
|
||||||
|
if (cert.length > 0) {
|
||||||
|
return cert[1].replace(/[\n|\r\n]/g, '');
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeWhitespace(xml) {
|
||||||
|
return xml.replace(/\r\n/g, '').replace(/\n/g,'').replace(/>(\s*)</g, '><').trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
var ASSERTION_NS = 'urn:oasis:names:tc:SAML:2.0:assertion';
|
||||||
|
var SAMLP_NS = 'urn:oasis:names:tc:SAML:2.0:protocol';
|
||||||
|
|
||||||
|
var algorithms = {
|
||||||
signature: {
|
signature: {
|
||||||
'rsa-sha256': 'http://www.w3.org/2001/04/xmldsig-more#rsa-sha256',
|
'rsa-sha256': 'http://www.w3.org/2001/04/xmldsig-more#rsa-sha256',
|
||||||
'rsa-sha1': 'http://www.w3.org/2000/09/xmldsig#rsa-sha1'
|
'rsa-sha1': 'http://www.w3.org/2000/09/xmldsig#rsa-sha1'
|
||||||
},
|
},
|
||||||
digest: {
|
digest: {
|
||||||
sha256: 'http://www.w3.org/2001/04/xmlenc#sha256',
|
'sha256': 'http://www.w3.org/2001/04/xmlenc#sha256',
|
||||||
sha1: 'http://www.w3.org/2000/09/xmldsig#sha1'
|
'sha1': 'http://www.w3.org/2000/09/xmldsig#sha1'
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
function pemToCert (pem) {
|
exports.parseRequest = function(options, request, callback) {
|
||||||
const cert = /-----BEGIN CERTIFICATE-----([^-]*)-----END CERTIFICATE-----/g.exec(pem.toString())
|
options.issuer = options.issuer || 'https://idp.sso.tools';
|
||||||
if (cert.length > 0) {
|
request = decodeURIComponent(request);
|
||||||
return cert[1].replace(/[\n|\r\n]/g, '')
|
var buffer = Buffer.from(request, 'base64');
|
||||||
}
|
const result = zlib.inflateRawSync(buffer)
|
||||||
|
var info = {};
|
||||||
return null
|
try {
|
||||||
}
|
buffer = result;
|
||||||
|
var doc = new Parser().parseFromString(buffer.toString());
|
||||||
function removeWhitespace (xml) {
|
var rootElement = doc.documentElement;
|
||||||
return xml.replace(/\r\n/g, '').replace(/\n/g, '').replace(/>(\s*)</g, '><').trim()
|
if (rootElement.localName == 'AuthnRequest') {
|
||||||
}
|
info.login = {};
|
||||||
|
info.login.callbackUrl = rootElement.getAttribute('AssertionConsumerServiceURL');
|
||||||
const idp = {
|
info.login.destination = rootElement.getAttribute('Destination');
|
||||||
|
info.login.id = rootElement.getAttribute('ID');
|
||||||
parseRequest (options, request, callback) {
|
info.login.forceAuthn = rootElement.getAttribute('ForceAuthn') === "true";
|
||||||
options.issuer = options.issuer || 'https://idp.sso.tools'
|
var issuer = rootElement.getElementsByTagNameNS(ASSERTION_NS, 'Issuer')[0];
|
||||||
request = decodeURIComponent(request)
|
|
||||||
let buffer = Buffer.from(request, 'base64')
|
|
||||||
const result = zlib.inflateRawSync(buffer)
|
|
||||||
const info = {}
|
|
||||||
try {
|
|
||||||
buffer = result
|
|
||||||
const doc = new DOMParser().parseFromString(buffer.toString())
|
|
||||||
const rootElement = doc.documentElement
|
|
||||||
if (rootElement.localName === 'AuthnRequest') {
|
|
||||||
info.login = {}
|
|
||||||
info.login.callbackUrl = rootElement.getAttribute('AssertionConsumerServiceURL')
|
|
||||||
info.login.destination = rootElement.getAttribute('Destination')
|
|
||||||
info.login.id = rootElement.getAttribute('ID')
|
|
||||||
info.login.forceAuthn = rootElement.getAttribute('ForceAuthn') === 'true'
|
|
||||||
const issuer = rootElement.getElementsByTagNameNS(ASSERTION_NS, 'Issuer')[0]
|
|
||||||
if (issuer) {
|
if (issuer) {
|
||||||
info.login.issuer = issuer.textContent
|
info.login.issuer = issuer.textContent;
|
||||||
}
|
}
|
||||||
const nameIDPolicy = rootElement.getElementsByTagNameNS(SAMLP_NS, 'NameIDPolicy')[0]
|
var nameIDPolicy = rootElement.getElementsByTagNameNS(SAMLP_NS, 'NameIDPolicy')[0];
|
||||||
if (nameIDPolicy) {
|
if (nameIDPolicy) {
|
||||||
info.login.nameIdentifierFormat = nameIDPolicy.getAttribute('Format')
|
info.login.nameIdentifierFormat = nameIDPolicy.getAttribute('Format');
|
||||||
}
|
}
|
||||||
const requestedAuthnContext = rootElement.getElementsByTagNameNS(SAMLP_NS, 'RequestedAuthnContext')[0]
|
var requestedAuthnContext = rootElement.getElementsByTagNameNS(SAMLP_NS, 'RequestedAuthnContext')[0];
|
||||||
if (requestedAuthnContext) {
|
if (requestedAuthnContext) {
|
||||||
const authnContextClassRef = requestedAuthnContext.getElementsByTagNameNS(ASSERTION_NS, 'AuthnContextClassRef')[0]
|
var authnContextClassRef = requestedAuthnContext.getElementsByTagNameNS(ASSERTION_NS, 'AuthnContextClassRef')[0];
|
||||||
if (authnContextClassRef) {
|
if (authnContextClassRef) {
|
||||||
info.login.authnContextClassRef = authnContextClassRef.textContent
|
info.login.authnContextClassRef = authnContextClassRef.textContent;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (rootElement.localName === 'LogoutRequest') {
|
} else if (rootElement.localName == 'LogoutRequest') {
|
||||||
info.logout = {}
|
info.logout = {};
|
||||||
info.logout.callbackUrl = options.callbackUrl
|
info.logout.callbackUrl = options.callbackUrl;
|
||||||
info.logout.destination = rootElement.getAttribute('Destination')
|
info.logout.destination = rootElement.getAttribute('Destination');
|
||||||
info.logout.id = rootElement.getAttribute('ID')
|
info.logout.id = rootElement.getAttribute('ID');
|
||||||
const issuerElem = rootElement.getElementsByTagNameNS(ASSERTION_NS, 'Issuer')[0]
|
const issuerElem = rootElement.getElementsByTagNameNS(ASSERTION_NS, 'Issuer')[0];
|
||||||
if (issuerElem) info.logout.issuer = issuerElem.textContent
|
if (issuerElem) info.logout.issuer = issuerElem.textContent;
|
||||||
const nameIdElem = rootElement.getElementsByTagNameNS(ASSERTION_NS, 'NameID')[0]
|
const nameIdElem = rootElement.getElementsByTagNameNS(ASSERTION_NS, 'NameID')[0];
|
||||||
if (nameIdElem) info.logout.nameId = nameIdElem.textContent
|
if (nameIdElem) info.logout.nameId = nameIdElem.textContent;
|
||||||
info.logout.response =
|
info.logout.response =
|
||||||
'<samlp:LogoutResponse xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" ' +
|
'<samlp:LogoutResponse xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" ' +
|
||||||
'xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" ID="_' + crypto.randomBytes(21).toString('hex') +
|
'xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" ID="_' + crypto.randomBytes(21).toString('hex') +
|
||||||
'" Version="2.0" IssueInstant="' + new Date().toISOString() + '" Destination="' + info.logout.callbackUrl + '">' +
|
'" Version="2.0" IssueInstant="' + new Date().toISOString() + '" Destination="' + info.logout.callbackUrl + '">' +
|
||||||
'<saml:Issuer>' + options.issuer + '</saml:Issuer>' +
|
'<saml:Issuer>' + options.issuer + '</saml:Issuer>' +
|
||||||
'<samlp:Status><samlp:StatusCode Value="urn:oasis:names:tc:SAML:2.0:status:Success"/></samlp:Status>' +
|
'<samlp:Status><samlp:StatusCode Value="urn:oasis:names:tc:SAML:2.0:status:Success"/></samlp:Status>' +
|
||||||
'</samlp:LogoutResponse>'
|
'</samlp:LogoutResponse>';
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch(e) { console.log(e)
|
||||||
console.log(e)
|
|
||||||
}
|
}
|
||||||
return info
|
return info
|
||||||
},
|
};
|
||||||
|
|
||||||
createAssertion (options) {
|
exports.createAssertion = function(options) {
|
||||||
if (!options.key) { throw new Error('Expecting a private key in pem format') }
|
if (!options.key)
|
||||||
|
throw new Error('Expecting a private key in pem format');
|
||||||
|
|
||||||
if (!options.cert) { throw new Error('Expecting a public key cert in pem format') }
|
if (!options.cert)
|
||||||
|
throw new Error('Expecting a public key cert in pem format');
|
||||||
|
|
||||||
options.signatureAlgorithm = options.signatureAlgorithm || 'rsa-sha256'
|
options.signatureAlgorithm = options.signatureAlgorithm || 'rsa-sha256';
|
||||||
options.digestAlgorithm = options.digestAlgorithm || 'sha256'
|
options.digestAlgorithm = options.digestAlgorithm || 'sha256';
|
||||||
|
|
||||||
const doc = new DOMParser().parseFromString(SAMLP.toString())
|
var doc = new Parser().parseFromString(samlp.toString());
|
||||||
|
|
||||||
doc.documentElement.setAttribute('ID', '_' + (options.uid || crypto.randomBytes(21).toString('hex')))
|
doc.documentElement.setAttribute('ID', '_' + (options.uid || crypto.randomBytes(21).toString('hex')));
|
||||||
if (options.issuer) {
|
if (options.issuer) {
|
||||||
const issuer = doc.documentElement.getElementsByTagName('saml:Issuer')
|
var issuer = doc.documentElement.getElementsByTagName('saml:Issuer');
|
||||||
issuer[0].textContent = options.issuer
|
issuer[0].textContent = options.issuer;
|
||||||
}
|
|
||||||
|
|
||||||
const now = new Date().toISOString()
|
|
||||||
doc.documentElement.setAttribute('IssueInstant', now)
|
|
||||||
const conditions = doc.documentElement.getElementsByTagName('saml:Conditions')[0]
|
|
||||||
const confirmationData = doc.documentElement.getElementsByTagName('saml:SubjectConfirmationData')[0]
|
|
||||||
|
|
||||||
if (options.lifetimeInSeconds) {
|
|
||||||
const expires = new Date(Date.now() + options.lifetimeInSeconds * 1000).toISOString()
|
|
||||||
conditions.setAttribute('NotBefore', now)
|
|
||||||
conditions.setAttribute('NotOnOrAfter', expires)
|
|
||||||
confirmationData.setAttribute('NotOnOrAfter', expires)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (options.audiences) {
|
|
||||||
const audienceRestrictionsElement = doc.createElementNS(ASSERTION_NS, 'saml:AudienceRestriction')
|
|
||||||
const audiences = options.audiences instanceof Array ? options.audiences : [options.audiences]
|
|
||||||
audiences.forEach(function (audience) {
|
|
||||||
const element = doc.createElementNS(ASSERTION_NS, 'saml:Audience')
|
|
||||||
element.textContent = audience
|
|
||||||
audienceRestrictionsElement.appendChild(element)
|
|
||||||
})
|
|
||||||
conditions.appendChild(audienceRestrictionsElement)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (options.recipient) { confirmationData.setAttribute('Recipient', options.recipient) }
|
|
||||||
|
|
||||||
if (options.inResponseTo) { confirmationData.setAttribute('InResponseTo', options.inResponseTo) }
|
|
||||||
|
|
||||||
if (options.attributes) {
|
|
||||||
const statement = doc.createElementNS(ASSERTION_NS, 'saml:AttributeStatement')
|
|
||||||
statement.setAttribute('xmlns:xs', 'http://www.w3.org/2001/XMLSchema')
|
|
||||||
statement.setAttribute('xmlns:xsi', 'http://www.w3.org/2001/XMLSchema-instance')
|
|
||||||
Object.keys(options.attributes).forEach(function (prop) {
|
|
||||||
if (typeof options.attributes[prop] === 'undefined') return
|
|
||||||
const attributeElement = doc.createElementNS(ASSERTION_NS, 'saml:Attribute')
|
|
||||||
attributeElement.setAttribute('Name', prop)
|
|
||||||
const values = options.attributes[prop] instanceof Array ? options.attributes[prop] : [options.attributes[prop]]
|
|
||||||
values.forEach(function (value) {
|
|
||||||
const valueElement = doc.createElementNS(ASSERTION_NS, 'saml:AttributeValue')
|
|
||||||
valueElement.setAttribute('xsi:type', 'xs:anyType')
|
|
||||||
valueElement.textContent = value
|
|
||||||
attributeElement.appendChild(valueElement)
|
|
||||||
})
|
|
||||||
|
|
||||||
if (values && values.length > 0) {
|
|
||||||
// saml:Attribute must have at least one saml:AttributeValue
|
|
||||||
statement.appendChild(attributeElement)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
doc.documentElement.appendChild(statement)
|
|
||||||
}
|
|
||||||
|
|
||||||
doc.getElementsByTagName('saml:AuthnStatement')[0].setAttribute('AuthnInstant', now)
|
|
||||||
|
|
||||||
if (options.sessionExpiration) {
|
|
||||||
doc.getElementsByTagName('saml:AuthnStatement')[0].setAttribute('SessionNotOnOrAfter', options.sessionExpiration)
|
|
||||||
}
|
|
||||||
if (options.sessionIndex) {
|
|
||||||
doc.getElementsByTagName('saml:AuthnStatement')[0].setAttribute('SessionIndex', options.sessionIndex)
|
|
||||||
}
|
|
||||||
|
|
||||||
const nameID = doc.documentElement.getElementsByTagNameNS(ASSERTION_NS, 'NameID')[0]
|
|
||||||
|
|
||||||
if (options.nameIdentifier) {
|
|
||||||
nameID.textContent = options.nameIdentifier
|
|
||||||
}
|
|
||||||
|
|
||||||
if (options.nameIdentifierFormat) {
|
|
||||||
nameID.setAttribute('Format', options.nameIdentifierFormat)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (options.authnContextClassRef) {
|
|
||||||
const authnCtxClassRef = doc.getElementsByTagName('saml:AuthnContextClassRef')[0]
|
|
||||||
authnCtxClassRef.textContent = options.authnContextClassRef
|
|
||||||
}
|
|
||||||
|
|
||||||
const sig = this.signDocument(doc.toString(), "//*[local-name(.)='Assertion']", options)
|
|
||||||
return sig.getSignedXml()
|
|
||||||
},
|
|
||||||
|
|
||||||
signDocument (token, reference, options) {
|
|
||||||
options.signatureAlgorithm = options.signatureAlgorithm || 'rsa-sha256'
|
|
||||||
options.digestAlgorithm = options.digestAlgorithm || 'sha256'
|
|
||||||
token = removeWhitespace(token)
|
|
||||||
const cert = pemToCert(options.cert)
|
|
||||||
const sig = new SignedXml({
|
|
||||||
signatureAlgorithm: ALGORITHMS.signature[options.signatureAlgorithm],
|
|
||||||
digestAlgorithm: ALGORITHMS.digest[options.digestAlgorithm],
|
|
||||||
canonicalizationAlgorithm: 'http://www.w3.org/2001/10/xml-exc-c14n#',
|
|
||||||
idAttribute: 'ID',
|
|
||||||
privateKey: options.key
|
|
||||||
})
|
|
||||||
sig.addReference({
|
|
||||||
xpath: reference,
|
|
||||||
transforms: ['http://www.w3.org/2000/09/xmldsig#enveloped-signature', 'http://www.w3.org/2001/10/xml-exc-c14n#'],
|
|
||||||
digestAlgorithm: ALGORITHMS.digest[options.digestAlgorithm]
|
|
||||||
})
|
|
||||||
sig.keyInfoProvider = {
|
|
||||||
getKeyInfo: function (key, prefix) {
|
|
||||||
return '<' + prefix + ':X509Data><' + prefix + ':X509Certificate>' + cert + '</' + prefix + ':X509Certificate></' + prefix + ':X509Data>'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
sig.computeSignature(token, { prefix: 'ds', location: { action: 'after', reference: "//*[local-name(.)='Issuer']" } })
|
|
||||||
return sig
|
|
||||||
},
|
|
||||||
|
|
||||||
createResponse (options) {
|
|
||||||
let response = '<samlp:Response xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" Version="2.0"'
|
|
||||||
response += ' ID="_' + crypto.randomBytes(21).toString('hex') + '"'
|
|
||||||
response += ' IssueInstant="' + options.instant + '"'
|
|
||||||
if (options.inResponseTo) {
|
|
||||||
response += ' InResponseTo="' + options.inResponseTo + '"'
|
|
||||||
}
|
|
||||||
if (options.destination) {
|
|
||||||
response += ' Destination="' + options.destination + '"'
|
|
||||||
}
|
|
||||||
response += '><saml:Issuer>' + options.issuer + '</saml:Issuer>'
|
|
||||||
response += '<samlp:Status><samlp:StatusCode Value="' + options.samlStatusCode + '"/>'
|
|
||||||
if (options.samlStatusMessage) {
|
|
||||||
response += '<samlp:StatusMessage>' + options.samlStatusMessage + '</samlp:StatusMessage>'
|
|
||||||
}
|
|
||||||
response += '</samlp:Status>'
|
|
||||||
response += options.assertion
|
|
||||||
response += '</samlp:Response>'
|
|
||||||
|
|
||||||
return response
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var now = new Date().toISOString();
|
||||||
|
doc.documentElement.setAttribute('IssueInstant', now);
|
||||||
|
var conditions = doc.documentElement.getElementsByTagName('saml:Conditions')[0];
|
||||||
|
var confirmationData = doc.documentElement.getElementsByTagName('saml:SubjectConfirmationData')[0];
|
||||||
|
|
||||||
|
if (options.lifetimeInSeconds) {
|
||||||
|
var expires = new Date(Date.now() + options.lifetimeInSeconds*1000).toISOString();
|
||||||
|
conditions.setAttribute('NotBefore', now);
|
||||||
|
conditions.setAttribute('NotOnOrAfter', expires);
|
||||||
|
confirmationData.setAttribute('NotOnOrAfter', expires);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.audiences) {
|
||||||
|
var audienceRestrictionsElement = doc.createElementNS(ASSERTION_NS, 'saml:AudienceRestriction');
|
||||||
|
var audiences = options.audiences instanceof Array ? options.audiences : [options.audiences];
|
||||||
|
audiences.forEach(function (audience) {
|
||||||
|
var element = doc.createElementNS(ASSERTION_NS, 'saml:Audience');
|
||||||
|
element.textContent = audience;
|
||||||
|
audienceRestrictionsElement.appendChild(element);
|
||||||
|
});
|
||||||
|
conditions.appendChild(audienceRestrictionsElement);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.recipient)
|
||||||
|
confirmationData.setAttribute('Recipient', options.recipient);
|
||||||
|
|
||||||
|
if (options.inResponseTo)
|
||||||
|
confirmationData.setAttribute('InResponseTo', options.inResponseTo);
|
||||||
|
|
||||||
|
if (options.attributes) {
|
||||||
|
var statement = doc.createElementNS(ASSERTION_NS, 'saml:AttributeStatement');
|
||||||
|
statement.setAttribute('xmlns:xs', 'http://www.w3.org/2001/XMLSchema');
|
||||||
|
statement.setAttribute('xmlns:xsi', 'http://www.w3.org/2001/XMLSchema-instance');
|
||||||
|
Object.keys(options.attributes).forEach(function(prop) {
|
||||||
|
if(typeof options.attributes[prop] === 'undefined') return;
|
||||||
|
var attributeElement = doc.createElementNS(ASSERTION_NS, 'saml:Attribute');
|
||||||
|
attributeElement.setAttribute('Name', prop);
|
||||||
|
var values = options.attributes[prop] instanceof Array ? options.attributes[prop] : [options.attributes[prop]];
|
||||||
|
values.forEach(function (value) {
|
||||||
|
var valueElement = doc.createElementNS(ASSERTION_NS, 'saml:AttributeValue');
|
||||||
|
valueElement.setAttribute('xsi:type', 'xs:anyType');
|
||||||
|
valueElement.textContent = value;
|
||||||
|
attributeElement.appendChild(valueElement);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (values && values.length > 0) {
|
||||||
|
// saml:Attribute must have at least one saml:AttributeValue
|
||||||
|
statement.appendChild(attributeElement);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
doc.documentElement.appendChild(statement);
|
||||||
|
}
|
||||||
|
|
||||||
|
doc.getElementsByTagName('saml:AuthnStatement')[0].setAttribute('AuthnInstant', now);
|
||||||
|
|
||||||
|
if (options.sessionExpiration) {
|
||||||
|
doc.getElementsByTagName('saml:AuthnStatement')[0].setAttribute('SessionNotOnOrAfter', options.sessionExpiration);
|
||||||
|
}
|
||||||
|
if (options.sessionIndex) {
|
||||||
|
doc.getElementsByTagName('saml:AuthnStatement')[0].setAttribute('SessionIndex', options.sessionIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
var nameID = doc.documentElement.getElementsByTagNameNS(ASSERTION_NS, 'NameID')[0];
|
||||||
|
|
||||||
|
if (options.nameIdentifier) {
|
||||||
|
nameID.textContent = options.nameIdentifier;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.nameIdentifierFormat) {
|
||||||
|
nameID.setAttribute('Format', options.nameIdentifierFormat);
|
||||||
|
}
|
||||||
|
|
||||||
|
if( options.authnContextClassRef ) {
|
||||||
|
var authnCtxClassRef = doc.getElementsByTagName('saml:AuthnContextClassRef')[0];
|
||||||
|
authnCtxClassRef.textContent = options.authnContextClassRef;
|
||||||
|
}
|
||||||
|
|
||||||
|
var sig = exports.signDocument(doc.toString(), "//*[local-name(.)='Assertion']", options);
|
||||||
|
return sig.getSignedXml();
|
||||||
|
};
|
||||||
|
|
||||||
|
exports.signDocument = function(token, reference, options) {
|
||||||
|
options.signatureAlgorithm = options.signatureAlgorithm || 'rsa-sha256';
|
||||||
|
options.digestAlgorithm = options.digestAlgorithm || 'sha256';
|
||||||
|
token = removeWhitespace(token);
|
||||||
|
var cert = pemToCert(options.cert);
|
||||||
|
var sig = new SignedXml(null, { signatureAlgorithm: algorithms.signature[options.signatureAlgorithm], idAttribute: 'ID' });
|
||||||
|
sig.signingKey = options.key;
|
||||||
|
sig.addReference(reference,
|
||||||
|
["http://www.w3.org/2000/09/xmldsig#enveloped-signature", "http://www.w3.org/2001/10/xml-exc-c14n#"],
|
||||||
|
algorithms.digest[options.digestAlgorithm]);
|
||||||
|
sig.keyInfoProvider = {
|
||||||
|
getKeyInfo: function (key, prefix) {
|
||||||
|
return '<'+prefix+':X509Data><'+prefix+':X509Certificate>' + cert + '</'+prefix+':X509Certificate></'+prefix+':X509Data>';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
sig.computeSignature(token, {prefix: 'ds', location: {action: 'after', reference : "//*[local-name(.)='Issuer']"}});
|
||||||
|
return sig;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default idp
|
exports.createResponse = function(options) {
|
||||||
|
var response = '<samlp:Response xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" Version="2.0"';
|
||||||
|
response += ' ID="_' + crypto.randomBytes(21).toString('hex') + '"';
|
||||||
|
response += ' IssueInstant="' + options.instant + '"';
|
||||||
|
if (options.inResponseTo) {
|
||||||
|
response += ' InResponseTo="' + options.inResponseTo + '"';
|
||||||
|
}
|
||||||
|
if (options.destination) {
|
||||||
|
response += ' Destination="' + options.destination + '"';
|
||||||
|
}
|
||||||
|
response += '><saml:Issuer>' + options.issuer + '</saml:Issuer>';
|
||||||
|
response += '<samlp:Status><samlp:StatusCode Value="' + options.samlStatusCode + '"/>';
|
||||||
|
if (options.samlStatusMessage) {
|
||||||
|
response += '<samlp:StatusMessage>' + options.samlStatusMessage + '</samlp:StatusMessage>';
|
||||||
|
}
|
||||||
|
response += '</samlp:Status>';
|
||||||
|
response += options.assertion;
|
||||||
|
response += '</samlp:Response>';
|
||||||
|
|
||||||
|
return response;
|
||||||
|
};
|
||||||
|
787
idp/index.js
787
idp/index.js
File diff suppressed because it is too large
Load Diff
@ -1,11 +0,0 @@
|
|||||||
import * as Sentry from '@sentry/node'
|
|
||||||
import { nodeProfilingIntegration } from '@sentry/profiling-node'
|
|
||||||
|
|
||||||
Sentry.init({
|
|
||||||
dsn: 'https://0bc5040f94640cb1084cb27132265702@o4508066290532352.ingest.de.sentry.io/4508066312683600',
|
|
||||||
integrations: [
|
|
||||||
nodeProfilingIntegration()
|
|
||||||
],
|
|
||||||
tracesSampleRate: 1.0,
|
|
||||||
profilesSampleRate: 1.0
|
|
||||||
})
|
|
@ -1,28 +1,31 @@
|
|||||||
{
|
{
|
||||||
"name": "ssotoolsidp",
|
"name": "ssotoolsidp",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"type": "module",
|
"scripts": {},
|
||||||
"scripts": {
|
|
||||||
"start": "nodemon index.js",
|
|
||||||
"lint": "standard --fix"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@sentry/node": "^8.33.1",
|
"async": "^3.2.4",
|
||||||
"@sentry/profiling-node": "^8.33.1",
|
|
||||||
"bcryptjs": "^2.4.3",
|
"bcryptjs": "^2.4.3",
|
||||||
"body-parser": "^1.20.3",
|
"body-parser": "^1.19.0",
|
||||||
"cookie-parser": "^1.4.6",
|
"cookie-parser": "^1.4.5",
|
||||||
"crypto": "^1.0.1",
|
"crypto": "^1.0.1",
|
||||||
"express": "^4.21.0",
|
"debug": "^4.1.1",
|
||||||
"jsonwebtoken": "^9.0.2",
|
"express": "^4.17.1",
|
||||||
"mongodb": "^6.9.0",
|
"jsonwebtoken": "^8.5.1",
|
||||||
"uuid": "^10.0.0",
|
"mongodb": "^5.3.0",
|
||||||
"xml-crypto": "^6.0.0",
|
"pem": "^1.13.2",
|
||||||
"xmldom": "^0.6.0",
|
"saml2-js": "^2.0.3",
|
||||||
|
"underscore": "^1.9.1",
|
||||||
|
"url": "^0.11.0",
|
||||||
|
"util": "^0.11.1",
|
||||||
|
"uuid": "^3.3.2",
|
||||||
|
"xml-crypto": "^1.1.1",
|
||||||
|
"xml-encryption": "^0.11.2",
|
||||||
|
"xml2js": "^0.4.19",
|
||||||
|
"xmlbuilder": "^10.1.1",
|
||||||
|
"xmldom": "^0.1.27",
|
||||||
"zlib": "^1.0.5"
|
"zlib": "^1.0.5"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"nodemon": "^3.1.7",
|
"nodemon": "^2.0.7"
|
||||||
"standard": "^17.1.2"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
3509
idp/yarn.lock
3509
idp/yarn.lock
File diff suppressed because it is too large
Load Diff
@ -2,26 +2,22 @@
|
|||||||
"name": "web",
|
"name": "web",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "vite --port 3800",
|
"start": "vite --port 3800",
|
||||||
"build": "vite build",
|
"build": "vite build"
|
||||||
"lint": "standard --fix"
|
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@mdi/font": "^7.0.96",
|
"@mdi/font": "^7.0.96",
|
||||||
"@sentry/vue": "^8.33.1",
|
|
||||||
"moment": "^2.29.4",
|
"moment": "^2.29.4",
|
||||||
"vue": "^3.5.11",
|
"vue": "^3.2.45",
|
||||||
"vue-router": "^4.4.5",
|
"vue-router": "^4.1.6",
|
||||||
"vuetify": "^3.7.2",
|
"vuetify": "^3.0.1",
|
||||||
"vuex": "^4.1.0"
|
"vuex": "^4.1.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@vitejs/plugin-vue": "^5.1.4",
|
"@vitejs/plugin-vue": "^3.2.0",
|
||||||
"standard": "^17.1.2",
|
"vite": "^3.2.4",
|
||||||
"vite": "^5.4.8",
|
"vue-template-compiler": "^2.7.14"
|
||||||
"vue-template-compiler": "^2.7.16"
|
|
||||||
},
|
},
|
||||||
"browserslist": [
|
"browserslist": [
|
||||||
"> 1%",
|
"> 1%",
|
||||||
|
@ -142,10 +142,6 @@
|
|||||||
<v-card-text>
|
<v-card-text>
|
||||||
<p>No problem. Enter the email address of your account below, and if it exists we'll send a password-reset link to you.</p>
|
<p>No problem. Enter the email address of your account below, and if it exists we'll send a password-reset link to you.</p>
|
||||||
<v-text-field class="mt-5" type="email" label="Email address" v-model="loginData.email" />
|
<v-text-field class="mt-5" type="email" label="Email address" v-model="loginData.email" />
|
||||||
<v-alert v-if="resettingPasswordError" type="error">
|
|
||||||
<h4>There was a problem resetting your password</h4>
|
|
||||||
<p>{{resettingPasswordError}}</p>
|
|
||||||
</v-alert>
|
|
||||||
</v-card-text>
|
</v-card-text>
|
||||||
<v-card-actions>
|
<v-card-actions>
|
||||||
<v-spacer></v-spacer>
|
<v-spacer></v-spacer>
|
||||||
@ -179,7 +175,6 @@ export default {
|
|||||||
showPassword: false,
|
showPassword: false,
|
||||||
forgottenPasswordOpen: false,
|
forgottenPasswordOpen: false,
|
||||||
resettingPassword: false,
|
resettingPassword: false,
|
||||||
resettingPasswordError: null
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
@ -285,9 +280,8 @@ export default {
|
|||||||
api.req('POST', '/accounts/password/reset', { email: this.loginData.email }, () => {
|
api.req('POST', '/accounts/password/reset', { email: this.loginData.email }, () => {
|
||||||
this.resettingPassword = false;
|
this.resettingPassword = false;
|
||||||
this.forgottenPasswordOpen = false;
|
this.forgottenPasswordOpen = false;
|
||||||
}, err => {
|
}, () => {
|
||||||
this.resettingPassword = false;
|
this.resettingPassword = false;
|
||||||
this.resettingPasswordError = err.message;
|
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
@ -1,45 +1,45 @@
|
|||||||
const hosts = {
|
const hosts = {
|
||||||
development: 'http://localhost:3801',
|
'development': 'http://localhost:3801',
|
||||||
production: 'https://api.sso.tools'
|
'production': 'https://api.sso.tools',
|
||||||
}
|
};
|
||||||
|
|
||||||
export const api = {
|
export const api = {
|
||||||
|
|
||||||
token: null,
|
token: null,
|
||||||
|
|
||||||
req (method, path, data, success, fail) {
|
req(method, path, data, success, fail) {
|
||||||
const xhr = new window.XMLHttpRequest()
|
const xhr = new XMLHttpRequest();
|
||||||
xhr.open(method, `${hosts[process.env.NODE_ENV]}${path}`)
|
xhr.open(method, `${hosts[process.env.NODE_ENV]}${path}`);
|
||||||
xhr.setRequestHeader('Content-Type', 'application/json')
|
xhr.setRequestHeader('Content-Type', 'application/json');
|
||||||
if (api.token) {
|
if (api.token) {
|
||||||
xhr.setRequestHeader('Authorization', `Bearer ${api.token}`)
|
xhr.setRequestHeader('Authorization', `Bearer ${api.token}`);
|
||||||
}
|
}
|
||||||
xhr.onreadystatechange = () => {
|
xhr.onreadystatechange = () => {
|
||||||
if (xhr.readyState === 4) {
|
if (xhr.readyState === 4) {
|
||||||
if (xhr.status === 200) {
|
if (xhr.status === 200) {
|
||||||
let response
|
let response;
|
||||||
try { response = JSON.parse(xhr.responseText) } catch (err) { console.log(err) }
|
try { response = JSON.parse(xhr.responseText); } catch (err) { console.log(err); }
|
||||||
if (success) success(response)
|
if (success) success(response);
|
||||||
} else {
|
} else {
|
||||||
if (xhr.status === 401) {
|
if (xhr.status === 401) {
|
||||||
return fail && fail({ status: 401, message: 'Authorisation is needed' })
|
return fail && fail({ status: 401, message: 'Authorisation is needed' });
|
||||||
}
|
}
|
||||||
let message
|
let message;
|
||||||
try { message = JSON.parse(xhr.responseText).message } catch (err) { if (fail) fail({ status: xhr.status, message: 'There was a problem with this request' }) }
|
try { message = JSON.parse(xhr.responseText).message; } catch (err) { if (fail) fail({ status: xhr.status, message: 'There was a problem with this request' }); }
|
||||||
if (fail) fail({ status: xhr.status, message })
|
if (fail) fail({ status: xhr.status, message });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
xhr.send(data && JSON.stringify(data))
|
xhr.send(data && JSON.stringify(data));
|
||||||
},
|
},
|
||||||
|
|
||||||
unauthenticatedRequest (method, path, data, success, fail, options) {
|
unauthenticatedRequest(method, path, data, success, fail, options) {
|
||||||
api.req(method, path, data, success, fail, false, options)
|
api.req(method, path, data, success, fail, false, options);
|
||||||
},
|
},
|
||||||
|
|
||||||
authenticatedRequest (method, path, data, success, fail, options) {
|
authenticatedRequest(method, path, data, success, fail, options ) {
|
||||||
api.req(method, path, data, success, fail, true, options)
|
api.req(method, path, data, success, fail, true, options);
|
||||||
}
|
},
|
||||||
}
|
};
|
||||||
|
|
||||||
export default api
|
export default api;
|
||||||
|
@ -8,8 +8,6 @@
|
|||||||
<p>A globally unique machine-friendly (alphanumeric) code to identify this IDP on SSO Tools. We derive the issuer and other SSO information from this code, so you may need to update other configurations elsewhere if you do change it.</p>
|
<p>A globally unique machine-friendly (alphanumeric) code to identify this IDP on SSO Tools. We derive the issuer and other SSO information from this code, so you may need to update other configurations elsewhere if you do change it.</p>
|
||||||
<v-text-field v-model="idp.code" label="Code" />
|
<v-text-field v-model="idp.code" label="Code" />
|
||||||
|
|
||||||
<v-alert v-if="errorMessage" type="error">{{errorMessage}}</v-alert>
|
|
||||||
|
|
||||||
<v-btn color='primary' :loading="saving" v-on:click="save">Save changes</v-btn>
|
<v-btn color='primary' :loading="saving" v-on:click="save">Save changes</v-btn>
|
||||||
|
|
||||||
<v-snackbar v-model="snackbar" :timeout="2000" >Settings saved</v-snackbar>
|
<v-snackbar v-model="snackbar" :timeout="2000" >Settings saved</v-snackbar>
|
||||||
@ -26,7 +24,6 @@ export default {
|
|||||||
return {
|
return {
|
||||||
snackbar: false,
|
snackbar: false,
|
||||||
saving: false,
|
saving: false,
|
||||||
errorMessage: null
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
@ -34,14 +31,12 @@ export default {
|
|||||||
const { name, code } = this.idp;
|
const { name, code } = this.idp;
|
||||||
const data = { name, code };
|
const data = { name, code };
|
||||||
this.saving = true;
|
this.saving = true;
|
||||||
this.errorMessage = null;
|
|
||||||
api.req('PUT', `/idps/${this.idp._id}`, { name, code }, resp => {
|
api.req('PUT', `/idps/${this.idp._id}`, { name, code }, resp => {
|
||||||
this.$emit('onUpdateIdp', resp);
|
this.$emit('onUpdateIdp', resp);
|
||||||
this.snackbar = true;
|
this.snackbar = true;
|
||||||
this.saving = false;
|
this.saving = false;
|
||||||
}, err => {
|
}, err => {
|
||||||
this.saving = false;
|
this.saving = false;
|
||||||
this.errorMessage = err.message;
|
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -72,8 +72,6 @@
|
|||||||
|
|
||||||
<h3 class="mb-2">OAuth2 settings (optional)</h3>
|
<h3 class="mb-2">OAuth2 settings (optional)</h3>
|
||||||
<v-text-field class="mb-2" label="Redirect URI" v-model="newSP.oauth2RedirectUri" hint="The URI users will be redirected to after successful authorization." placeholder="https://myapp.com/oauth/callback"/>
|
<v-text-field class="mb-2" label="Redirect URI" v-model="newSP.oauth2RedirectUri" hint="The URI users will be redirected to after successful authorization." placeholder="https://myapp.com/oauth/callback"/>
|
||||||
|
|
||||||
<v-alert v-if="createError" type="error">{{createError}}</v-alert>
|
|
||||||
</v-card-text>
|
</v-card-text>
|
||||||
<v-card-actions>
|
<v-card-actions>
|
||||||
<v-spacer></v-spacer>
|
<v-spacer></v-spacer>
|
||||||
@ -100,7 +98,6 @@ export default {
|
|||||||
newSP: { name: '', entityId: '', serviceUrl: '', callbackUrl: '', logoutUrl: '', logoutCallbackUrl: '', oauth2RedirectUri: ''},
|
newSP: { name: '', entityId: '', serviceUrl: '', callbackUrl: '', logoutUrl: '', logoutCallbackUrl: '', oauth2RedirectUri: ''},
|
||||||
dialog: false,
|
dialog: false,
|
||||||
editing: false,
|
editing: false,
|
||||||
createError: null,
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
created () {
|
created () {
|
||||||
@ -119,7 +116,6 @@ export default {
|
|||||||
create (event) {
|
create (event) {
|
||||||
const { _id, name, entityId, serviceUrl, callbackUrl, logoutUrl, logoutCallbackUrl, oauth2RedirectUri } = this.newSP;
|
const { _id, name, entityId, serviceUrl, callbackUrl, logoutUrl, logoutCallbackUrl, oauth2RedirectUri } = this.newSP;
|
||||||
const data = {name, entityId, serviceUrl, callbackUrl, logoutUrl, logoutCallbackUrl, oauth2RedirectUri};
|
const data = {name, entityId, serviceUrl, callbackUrl, logoutUrl, logoutCallbackUrl, oauth2RedirectUri};
|
||||||
this.createError = null;
|
|
||||||
if (_id && this.editing) {
|
if (_id && this.editing) {
|
||||||
api.req('PUT', `/idps/${this.idp._id}/sps/${_id}`, data, resp => {
|
api.req('PUT', `/idps/${this.idp._id}/sps/${_id}`, data, resp => {
|
||||||
this.sps.map(s => {
|
this.sps.map(s => {
|
||||||
@ -128,16 +124,12 @@ export default {
|
|||||||
});
|
});
|
||||||
this.dialog = false;
|
this.dialog = false;
|
||||||
this.editing = false;
|
this.editing = false;
|
||||||
}, err => {
|
}, err => console.log(err));
|
||||||
this.createError = err.message;
|
|
||||||
});
|
|
||||||
} else {
|
} else {
|
||||||
api.req('POST', `/idps/${this.idp._id}/sps`, data, resp => {
|
api.req('POST', `/idps/${this.idp._id}/sps`, data, resp => {
|
||||||
this.sps.push(resp);
|
this.sps.push(resp);
|
||||||
this.dialog = false;
|
this.dialog = false;
|
||||||
}, err => {
|
}, err => console.log(err));
|
||||||
this.createError = err.message;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
editSp(sp) {
|
editSp(sp) {
|
||||||
|
@ -68,7 +68,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<v-alert v-if="createError" type="error" dismissible>{{createError}}</v-alert>
|
|
||||||
</v-card-text>
|
</v-card-text>
|
||||||
<v-card-actions>
|
<v-card-actions>
|
||||||
<v-spacer></v-spacer>
|
<v-spacer></v-spacer>
|
||||||
@ -133,8 +132,6 @@
|
|||||||
<v-text-field class="mb-2" label="Attribute key" v-model="newAttribute.samlMapping" hint="Values for this attribute will be sent with this key during the SSO process." />
|
<v-text-field class="mb-2" label="Attribute key" v-model="newAttribute.samlMapping" hint="Values for this attribute will be sent with this key during the SSO process." />
|
||||||
<v-text-field label="Default value" v-model="newAttribute.defaultValue" hint="If not overridden in the user itself, this value will be sent as a default."/>
|
<v-text-field label="Default value" v-model="newAttribute.defaultValue" hint="If not overridden in the user itself, this value will be sent as a default."/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<v-alert v-if="createAttributeError" type="error" dismissible>{{createAttributeError}}</v-alert>
|
|
||||||
</v-card-text>
|
</v-card-text>
|
||||||
<v-card-actions>
|
<v-card-actions>
|
||||||
<v-spacer></v-spacer>
|
<v-spacer></v-spacer>
|
||||||
@ -169,8 +166,6 @@ export default {
|
|||||||
editing: false,
|
editing: false,
|
||||||
attributeDialog: false,
|
attributeDialog: false,
|
||||||
editingAttribute: false,
|
editingAttribute: false,
|
||||||
createError: null,
|
|
||||||
createAttributeError: null,
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
created () {
|
created () {
|
||||||
@ -199,7 +194,6 @@ export default {
|
|||||||
create (event) {
|
create (event) {
|
||||||
const { _id, firstName, lastName, email, password, attributes } = this.newUser;
|
const { _id, firstName, lastName, email, password, attributes } = this.newUser;
|
||||||
const data = {firstName, lastName, email, password, attributes};
|
const data = {firstName, lastName, email, password, attributes};
|
||||||
this.createError = null;
|
|
||||||
if (_id && this.editing) {
|
if (_id && this.editing) {
|
||||||
api.req('PUT', `/idps/${this.idp._id}/users/${_id}`, data, resp => {
|
api.req('PUT', `/idps/${this.idp._id}/users/${_id}`, data, resp => {
|
||||||
this.users.map(u => {
|
this.users.map(u => {
|
||||||
@ -208,22 +202,17 @@ export default {
|
|||||||
});
|
});
|
||||||
this.dialog = false;
|
this.dialog = false;
|
||||||
this.editing = false;
|
this.editing = false;
|
||||||
}, err => {
|
}, err => console.log(err));
|
||||||
this.createError = err.message;
|
|
||||||
});
|
|
||||||
} else {
|
} else {
|
||||||
api.req('POST', `/idps/${this.idp._id}/users`, data, resp => {
|
api.req('POST', `/idps/${this.idp._id}/users`, data, resp => {
|
||||||
this.users.push(resp);
|
this.users.push(resp);
|
||||||
this.dialog = false;
|
this.dialog = false;
|
||||||
}, err => {
|
}, err => console.log(err));
|
||||||
this.createError = err.message;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
createAttribute() {
|
createAttribute() {
|
||||||
const { _id, name, defaultValue, samlMapping } = this.newAttribute;
|
const { _id, name, defaultValue, samlMapping } = this.newAttribute;
|
||||||
const data = {name, defaultValue, samlMapping};
|
const data = {name, defaultValue, samlMapping};
|
||||||
this.createAttributeError = null;
|
|
||||||
if (_id && this.editingAttribute) {
|
if (_id && this.editingAttribute) {
|
||||||
api.req('PUT', `/idps/${this.idp._id}/attributes/${_id}`, data, resp => {
|
api.req('PUT', `/idps/${this.idp._id}/attributes/${_id}`, data, resp => {
|
||||||
this.attributes.map(u => {
|
this.attributes.map(u => {
|
||||||
@ -232,16 +221,12 @@ export default {
|
|||||||
});
|
});
|
||||||
this.attributeDialog = false;
|
this.attributeDialog = false;
|
||||||
this.editingAttribute = false;
|
this.editingAttribute = false;
|
||||||
}, err => {
|
}, err => console.log(err));
|
||||||
this.createAttributeError = err.message;
|
|
||||||
});
|
|
||||||
} else {
|
} else {
|
||||||
api.req('POST', `/idps/${this.idp._id}/attributes`, data, resp => {
|
api.req('POST', `/idps/${this.idp._id}/attributes`, data, resp => {
|
||||||
this.attributes.push(resp);
|
this.attributes.push(resp);
|
||||||
this.attributeDialog = false;
|
this.attributeDialog = false;
|
||||||
}, err => {
|
}, err => console.log(err));
|
||||||
this.createAttributeError = err.message;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
editUser(user) {
|
editUser(user) {
|
||||||
|
@ -7,7 +7,7 @@
|
|||||||
<div v-if="!success">
|
<div v-if="!success">
|
||||||
<v-text-field :type="showPassword ? 'text' : 'password'" label="Password" v-model="password" :append-icon="showPassword ? 'visibility' : 'visibility_off'" @click:append="showPassword = !showPassword"/>
|
<v-text-field :type="showPassword ? 'text' : 'password'" label="Password" v-model="password" :append-icon="showPassword ? 'visibility' : 'visibility_off'" @click:append="showPassword = !showPassword"/>
|
||||||
|
|
||||||
<v-alert v-if="error" type="error" class="mt-5 mb-5">
|
<v-alert :value="error" type="error">
|
||||||
<h4>Unable to set the password</h4>
|
<h4>Unable to set the password</h4>
|
||||||
<p>{{error}}</p>
|
<p>{{error}}</p>
|
||||||
</v-alert>
|
</v-alert>
|
||||||
@ -15,10 +15,10 @@
|
|||||||
<v-btn :loading="settingPassword" color="primary" @click="setPassword">Set new password</v-btn>
|
<v-btn :loading="settingPassword" color="primary" @click="setPassword">Set new password</v-btn>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<v-alert v-if="success" type="success" class="mt-5">
|
<v-alert :value="success" type="success">
|
||||||
<h4>Password updated successfully</h4>
|
<h4>Password updated successfully</h4>
|
||||||
<p>When you're ready, you can login with your new password.</p>
|
<p>When you're ready, you can login with your new password.</p>
|
||||||
<v-btn color="primary" class="mt-2" @click="e => $store.commit('openLogin', true)">Login</v-btn>
|
<v-btn color="primary" @click="e => $store.commit('openLogin', true)">Login</v-btn>
|
||||||
</v-alert>
|
</v-alert>
|
||||||
</v-container>
|
</v-container>
|
||||||
</template>
|
</template>
|
||||||
|
118
web/src/main.js
118
web/src/main.js
@ -7,70 +7,69 @@ import * as components from 'vuetify/components'
|
|||||||
import * as directives from 'vuetify/directives'
|
import * as directives from 'vuetify/directives'
|
||||||
import { aliases, mdi } from 'vuetify/iconsets/mdi'
|
import { aliases, mdi } from 'vuetify/iconsets/mdi'
|
||||||
import '@mdi/font/css/materialdesignicons.css'
|
import '@mdi/font/css/materialdesignicons.css'
|
||||||
import * as Sentry from '@sentry/vue'
|
|
||||||
|
|
||||||
import App from './App.vue'
|
import App from './App.vue'
|
||||||
|
|
||||||
import Home from './components/Home.vue'
|
import Home from './components/Home.vue';
|
||||||
import ResetPassword from './components/ResetPassword.vue'
|
import ResetPassword from './components/ResetPassword.vue';
|
||||||
import Account from './components/Account.vue'
|
import Account from './components/Account.vue';
|
||||||
import Dashboard from './components/Dashboard.vue'
|
import Dashboard from './components/Dashboard.vue'
|
||||||
import Root from './components/Root.vue'
|
import Root from './components/Root.vue'
|
||||||
import NewIDP from './components/NewIDP.vue'
|
import NewIDP from './components/NewIDP.vue'
|
||||||
import IDP from './components/IDP.vue'
|
import IDP from './components/IDP.vue';
|
||||||
import IDPHome from './components/IdpHome.vue'
|
import IDPHome from './components/IdpHome.vue';
|
||||||
import IDPUsers from './components/IdpUsers.vue'
|
import IDPUsers from './components/IdpUsers.vue';
|
||||||
import IDPSettings from './components/IdpSettings.vue'
|
import IDPSettings from './components/IdpSettings.vue';
|
||||||
import IDPSPs from './components/IdpSps.vue'
|
import IDPSPs from './components/IdpSps.vue';
|
||||||
import IDPSAML from './components/IdpSaml.vue'
|
import IDPSAML from './components/IdpSaml.vue';
|
||||||
import IDPSAMLLogs from './components/IdpSamlLogs.vue'
|
import IDPSAMLLogs from './components/IdpSamlLogs.vue';
|
||||||
import IDPOAuth from './components/IdpOauth.vue'
|
import IDPOAuth from './components/IdpOauth.vue';
|
||||||
import IDPOAuthGuide from './components/IdpOauthGuide.vue'
|
import IDPOAuthGuide from './components/IdpOauthGuide.vue';
|
||||||
import IDPSaml2Guide from './components/IdpSaml2Guide.vue'
|
import IDPSaml2Guide from './components/IdpSaml2Guide.vue';
|
||||||
import IDPOAuthLogs from './components/IdpOauthLogs.vue'
|
import IDPOAuthLogs from './components/IdpOauthLogs.vue';
|
||||||
import GuideLayout from './components/Guide.vue'
|
import GuideLayout from './components/Guide.vue';
|
||||||
|
|
||||||
import PrivacyPolicy from './components/legal/PrivacyPolicy.vue'
|
import PrivacyPolicy from './components/legal/PrivacyPolicy.vue';
|
||||||
import TermsOfUse from './components/legal/TermsOfUse.vue'
|
import TermsOfUse from './components/legal/TermsOfUse.vue';
|
||||||
|
|
||||||
const store = createStore({
|
const store = createStore({
|
||||||
state: {
|
state: {
|
||||||
loggedIn: false,
|
loggedIn: false,
|
||||||
user: null,
|
user: null,
|
||||||
registerOpen: false,
|
registerOpen: false,
|
||||||
loginOpen: false
|
loginOpen: false,
|
||||||
},
|
},
|
||||||
mutations: {
|
mutations: {
|
||||||
login (state, loggedIn) {
|
login (state, loggedIn) {
|
||||||
state.loggedIn = loggedIn
|
state.loggedIn = loggedIn;
|
||||||
},
|
},
|
||||||
setUser (state, user) {
|
setUser (state, user) {
|
||||||
state.user = user
|
state.user = user;
|
||||||
if (user && window.drift && window.drift.identify) {
|
if (user && window.drift && window.drift.identify) {
|
||||||
window.drift.identify(user._id, {
|
window.drift.identify(user._id, {
|
||||||
email: user.email,
|
email: user.email,
|
||||||
firstName: user.firstName
|
firstName: user.firstName,
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
if (!user && window.drift && window.drift.reset) {
|
if (!user && window.drift && window.drift.reset) {
|
||||||
window.drift.reset()
|
window.drift.reset();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
updateProfile (state, profile) {
|
updateProfile (state, profile) {
|
||||||
state.user = Object.assign({}, state.user, profile)
|
state.user = Object.assign({}, state.user, profile);
|
||||||
},
|
},
|
||||||
openRegister (state, open) {
|
openRegister (state, open) {
|
||||||
state.registerOpen = open
|
state.registerOpen = open;
|
||||||
},
|
},
|
||||||
openLogin (state, open) {
|
openLogin (state, open) {
|
||||||
state.loginOpen = open
|
state.loginOpen = open;
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const router = createRouter({
|
const router = createRouter({
|
||||||
scrollBehavior () {
|
scrollBehavior() {
|
||||||
return { left: 0, top: 0 }
|
return { left: 0, top: 0 };
|
||||||
},
|
},
|
||||||
history: createWebHistory(),
|
history: createWebHistory(),
|
||||||
routes: [
|
routes: [
|
||||||
@ -80,32 +79,24 @@ const router = createRouter({
|
|||||||
{ path: '/account', component: Account },
|
{ path: '/account', component: Account },
|
||||||
{ path: '/password/reset', component: ResetPassword },
|
{ path: '/password/reset', component: ResetPassword },
|
||||||
{ path: '/dashboard', component: Dashboard },
|
{ path: '/dashboard', component: Dashboard },
|
||||||
{
|
{ path: '/guides', component: GuideLayout, children: [
|
||||||
path: '/guides',
|
{ path: 'oauth2', component: IDPOAuthGuide },
|
||||||
component: GuideLayout,
|
{ path: 'saml2', component: IDPSaml2Guide },
|
||||||
children: [
|
] },
|
||||||
{ path: 'oauth2', component: IDPOAuthGuide },
|
|
||||||
{ path: 'saml2', component: IDPSaml2Guide }
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{ path: '/idps/new', component: NewIDP },
|
{ path: '/idps/new', component: NewIDP },
|
||||||
{
|
{ path: '/idps/:id', component: IDP, children: [
|
||||||
path: '/idps/:id',
|
{ path: '', component: IDPHome },
|
||||||
component: IDP,
|
{ path: 'users', component: IDPUsers },
|
||||||
children: [
|
{ path: 'settings', component: IDPSettings },
|
||||||
{ path: '', component: IDPHome },
|
{ path: 'sps', component: IDPSPs },
|
||||||
{ path: 'users', component: IDPUsers },
|
{ path: 'saml', component: IDPSAML },
|
||||||
{ path: 'settings', component: IDPSettings },
|
{ path: 'saml/guide', component: IDPSaml2Guide },
|
||||||
{ path: 'sps', component: IDPSPs },
|
{ path: 'saml/logs', component: IDPSAMLLogs },
|
||||||
{ path: 'saml', component: IDPSAML },
|
{ path: 'oauth', component: IDPOAuth },
|
||||||
{ path: 'saml/guide', component: IDPSaml2Guide },
|
{ path: 'oauth/guide', component: IDPOAuthGuide },
|
||||||
{ path: 'saml/logs', component: IDPSAMLLogs },
|
{ path: 'oauth/logs', component: IDPOAuthLogs },
|
||||||
{ path: 'oauth', component: IDPOAuth },
|
] },
|
||||||
{ path: 'oauth/guide', component: IDPOAuthGuide },
|
{ path: '/root', component: Root },
|
||||||
{ path: 'oauth/logs', component: IDPOAuthLogs }
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{ path: '/root', component: Root }
|
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -116,25 +107,12 @@ const vuetify = createVuetify({
|
|||||||
defaultSet: 'mdi',
|
defaultSet: 'mdi',
|
||||||
aliases,
|
aliases,
|
||||||
sets: {
|
sets: {
|
||||||
mdi
|
mdi,
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const app = createApp(App)
|
const app = createApp(App)
|
||||||
|
|
||||||
Sentry.init({
|
|
||||||
app,
|
|
||||||
dsn: 'https://68f35a231add93109261ef91390c4c5f@o4508066290532352.ingest.de.sentry.io/4508066292760656',
|
|
||||||
integrations: [
|
|
||||||
Sentry.browserTracingIntegration({ router }),
|
|
||||||
Sentry.replayIntegration()
|
|
||||||
],
|
|
||||||
tracesSampleRate: 1.0, // Capture 100% of the transactions
|
|
||||||
replaysSessionSampleRate: 0.1,
|
|
||||||
replaysOnErrorSampleRate: 1.0
|
|
||||||
})
|
|
||||||
|
|
||||||
app.use(store)
|
app.use(store)
|
||||||
app.use(vuetify)
|
app.use(vuetify)
|
||||||
app.use(router)
|
app.use(router)
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
export default {
|
export default {
|
||||||
hasPermission (user, permission, scope) {
|
hasPermission(user, permission, scope) {
|
||||||
if (!user?.permissions || !permission) return false
|
if (!user?.permissions || !permission) return false;
|
||||||
return user.permissions[scope || 'global']?.indexOf(permission) > -1
|
return user.permissions[scope || 'global']?.indexOf(permission) > -1;
|
||||||
}
|
}
|
||||||
}
|
}
|
2646
web/yarn.lock
2646
web/yarn.lock
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue
Block a user