add login

Signed-off-by: Tiago Garcia <tiago.rgarcia@ua.pt>
This commit is contained in:
Tiago Garcia 2024-12-17 00:39:52 +00:00
parent 7a00abbd6f
commit ce436a0991
Signed by: TiagoRG
GPG Key ID: DFCD48E3F420DB42
19 changed files with 134 additions and 37 deletions

View File

@ -1,10 +1,14 @@
#!/bin/python3 #!/bin/python3
import base64
import os import os
import sys import sys
import argparse import argparse
import logging import logging
import json import json
import requests import requests
from cryptography.hazmat.primitives.serialization import load_pem_private_key
from cryptography.hazmat.primitives.asymmetric import padding
from cryptography.hazmat.primitives import hashes
from subject import main from subject import main
@ -36,15 +40,15 @@ def createSession(args):
args = parser.parse_args() args = parser.parse_args()
if not args.org or not args.username or not args.password or not args.credentials or not args.session: if not args.org or not args.username or not args.credentials or not args.session:
logger.error("Need organization, username, password, credentials and session file") logger.error("Need organization, username, credentials and session file")
sys.exit(1) sys.exit(1)
if (not os.path.isfile(BASE_DIR + args.credentials)): if not os.path.isfile(BASE_DIR + args.credentials):
logger.error("File '" + args.credentials + "' not found.") logger.error("File '" + args.credentials + "' not found.")
sys.exit(1) sys.exit(1)
session = {'org' : args.org, 'username' : args.username, 'password' : args.password, 'credentials_file' : args.credentials} session = {'org' : args.org, 'username' : args.username}
#print( type(json.dumps(session))) #print( type(json.dumps(session)))
@ -55,6 +59,29 @@ def createSession(args):
logger.error("Failed to obtain response from server") logger.error("Failed to obtain response from server")
sys.exit(-1) sys.exit(-1)
response = req.json()
challenge = response['challenge']
with open(BASE_DIR + args.credentials, 'rb') as f:
try:
key = load_pem_private_key(f.read(), password=args.password.encode("utf-8") if args.password else None)
except ValueError:
logger.error("Invalid password")
sys.exit(-1)
signature = key.sign(
challenge.encode('utf-8'),
padding.PKCS1v15(),
hashes.SHA256()
)
try:
req = requests.post(f'http://{state['REP_ADDRESS']}/user/login', json=json.dumps({'signature' : base64.b64encode(signature).decode('utf-8')}), headers={'Authorization': response['token']})
req.raise_for_status()
except requests.exceptions.RequestException as errex:
logger.error("Failed to obtain response from server")
sys.exit(-1)
with open(BASE_DIR + args.session, 'w') as f: with open(BASE_DIR + args.session, 'w') as f:
json.dump(req.json(), f) json.dump(req.json(), f)

View File

@ -30,7 +30,7 @@ def decryptFile(args):
sys.exit(1) sys.exit(1)
# If first argument is not a file or not found # If first argument is not a file or not found
if (not os.path.isfile(BASE_DIR + args.encrypted)): if not os.path.isfile(BASE_DIR + args.encrypted):
logger.error("File '" + args.encrypted + "' not found.") logger.error("File '" + args.encrypted + "' not found.")
sys.exit(1) sys.exit(1)
@ -42,7 +42,7 @@ def decryptFile(args):
print(args.metadata) print(args.metadata)
metadata = json.loads(args.metadata) metadata = json.loads(args.metadata)
content = symmetric_encryption.decrypt_file(metadata['nonce'].encode(), metadata['key'].encode(), BASE_DIR + args.encrypted) content = symmetric_encryption.decrypt_file(metadata['key'].encode(), BASE_DIR + args.encrypted)
# Send decrypted content to stdout # Send decrypted content to stdout
sys.stdout.write(content) sys.stdout.write(content)

View File

@ -63,7 +63,7 @@ def getDoc(args):
#Get file with file_handle provided by metadata #Get file with file_handle provided by metadata
try: try:
file = requests.get(f'http://{state['REP_ADDRESS']}/get/' + metadata['file_handle'] + '/content') file = requests.get(f'http://{state['REP_ADDRESS']}/file/get/' + metadata['file_handle'] + '/content')
file.raise_for_status() file.raise_for_status()
except requests.exceptions.RequestException as errex: except requests.exceptions.RequestException as errex:
logger.error("Failed to obtain response from server.") logger.error("Failed to obtain response from server.")

View File

@ -13,6 +13,9 @@ logger.setLevel(logging.INFO)
BASE_DIR = os.path.join(os.path.expanduser('~'), '.sio/') BASE_DIR = os.path.join(os.path.expanduser('~'), '.sio/')
if not os.path.exists(BASE_DIR):
os.makedirs(BASE_DIR)
# Generate a key pair for a subject # Generate a key pair for a subject
# password - file for public key, file for private key # password - file for public key, file for private key
def generateKeyPair(args): def generateKeyPair(args):

View File

@ -23,7 +23,7 @@ def reset(args):
parser.add_argument("-r", '--repo', nargs=1, help="Address:Port of the repository") parser.add_argument("-r", '--repo', nargs=1, help="Address:Port of the repository")
parser.add_argument("-v", '--verbose', help="Increase verbosity", action="store_true") parser.add_argument("-v", '--verbose', help="Increase verbosity", action="store_true")
parser.add_argument('password', nargs='?', default=None) parser.add_argument('password', nargs='?', default="123")
args = parser.parse_args() args = parser.parse_args()

View File

@ -40,7 +40,7 @@ def test_rep_list_orgs():
def test_rep_create_session(): def test_rep_create_session():
# Test the rep_create_session command # Test the rep_create_session command
process = subprocess.Popen(f"{DELIVERY_PATH}/client/bin/rep_create_session org1 username password pub.pem session.json", shell=True) process = subprocess.Popen(f"{DELIVERY_PATH}/client/bin/rep_create_session org1 username password priv.pem session.json", shell=True)
process.wait() process.wait()
assert process.returncode == 0 assert process.returncode == 0

View File

@ -34,7 +34,7 @@ def encrypt_file(input_file, output_file=None):
# Function to decrypt a file # Function to decrypt a file
def decrypt_file(key, input_file, output_file=None): def decrypt_file(key, input_file, output_file=None) -> str:
plaintext_content = b"" plaintext_content = b""
with open(input_file, 'rb') as infile: with open(input_file, 'rb') as infile:
@ -62,5 +62,5 @@ def decrypt_file(key, input_file, output_file=None):
# Finalize decryption # Finalize decryption
plaintext_content += decryptor.finalize() plaintext_content += decryptor.finalize()
return plaintext_content return plaintext_content.decode('utf-8', errors='ignore')

View File

@ -32,7 +32,7 @@ def index():
@app.route("/reset", methods=["POST"]) @app.route("/reset", methods=["POST"])
def reset(): def reset():
password = request.json["password"] password = request.json.get("password")
if password != "123": if password != "123":
return jsonify({"error": "Invalid password"}), 403 return jsonify({"error": "Invalid password"}), 403
try: try:

View File

@ -26,9 +26,9 @@ class File(db_connection.Model):
"name": self.name, "name": self.name,
"created_at": self.created_at, "created_at": self.created_at,
"acl": self.acl, "acl": self.acl,
"deleter_id": self.deleter_id,
"key": self.key, "key": self.key,
"alg": self.alg, "alg": self.alg,
"org": {"id": self.org.id, "name": self.org.name}, "org": {"id": self.org.id, "name": self.org.name},
"creator": {"id": self.creator.id, "username": self.creator.username}, "creator": {"id": self.creator.id, "username": self.creator.username},
"deleter": {"id": self.deleter.id, "username": self.deleter.username} if self.deleter else None
} }

View File

@ -10,6 +10,8 @@ class Session(db_connection.Model):
roles = db_connection.Column(db_connection.JSON, default=list) roles = db_connection.Column(db_connection.JSON, default=list)
created_at = db_connection.Column(db_connection.DateTime, server_default=db_connection.func.now()) created_at = db_connection.Column(db_connection.DateTime, server_default=db_connection.func.now())
updated_at = db_connection.Column(db_connection.DateTime, server_default=db_connection.func.now(), server_onupdate=db_connection.func.now()) updated_at = db_connection.Column(db_connection.DateTime, server_default=db_connection.func.now(), server_onupdate=db_connection.func.now())
challenge = db_connection.Column(db_connection.String(255), unique=True)
verified = db_connection.Column(db_connection.Boolean, default=False)
def to_dict(self): def to_dict(self):
return { return {
@ -18,5 +20,7 @@ class Session(db_connection.Model):
"org_id": self.org_id, "org_id": self.org_id,
"token": self.token, "token": self.token,
"created_at": self.created_at, "created_at": self.created_at,
"updated_at": self.updated_at "updated_at": self.updated_at,
"challenge": self.challenge,
"verified": self.verified
} }

View File

@ -19,6 +19,6 @@ class User(db_connection.Model):
"username": self.username, "username": self.username,
"full_name": self.full_name, "full_name": self.full_name,
"email": self.email, "email": self.email,
"role": self.role, "roles": self.roles,
"orgs": [{"id": org_id, "name": org_data["name"], "status": org_data["status"]} for org_id, org_data in self.orgs.items()], "orgs": [{"id": org_id, "name": org_data["name"], "status": org_data["status"]} for org_id, org_data in self.orgs.items()],
} }

View File

@ -40,7 +40,6 @@ def file_get_metadata(document_handle: str):
@file_bp.route("/upload/metadata", methods=["POST"]) @file_bp.route("/upload/metadata", methods=["POST"])
def file_upload_metadata(): def file_upload_metadata():
session_token = request.headers.get("Authorization") session_token = request.headers.get("Authorization")
print(session_token)
session = SessionService.validate_session(session_token, required_perms=[Perm.DOC_NEW]) session = SessionService.validate_session(session_token, required_perms=[Perm.DOC_NEW])
if isinstance(session, tuple): if isinstance(session, tuple):
return session return session

View File

@ -71,7 +71,7 @@ def role_list_perms(role):
return jsonify({"error": "Organization not found"}), 404 return jsonify({"error": "Organization not found"}), 404
try: try:
perms = RoleService.get_perms_for_role(org, role) perms = RoleService.get_perms_for_role(org, role, return_str=True)
except ValueError as e: except ValueError as e:
return jsonify({"error": str(e)}), 400 return jsonify({"error": str(e)}), 400
return jsonify(perms), 200 return jsonify(perms), 200

View File

@ -1,4 +1,7 @@
import base64
import json import json
from cryptography.exceptions import InvalidSignature
from flask import Blueprint, request, jsonify from flask import Blueprint, request, jsonify
from services import UserService, SessionService, OrganizationService, RoleService from services import UserService, SessionService, OrganizationService, RoleService
from utils import Perm from utils import Perm
@ -11,9 +14,7 @@ def user_login():
if type(data) is str: if type(data) is str:
data = json.loads(data) data = json.loads(data)
if "username" not in data or "org" not in data: if "username" in data and "org" in data:
return jsonify({"error": "Missing required fields"}), 400
user = UserService.get_user_by_username(data["username"]) user = UserService.get_user_by_username(data["username"])
if not user: if not user:
return jsonify({"error": "User not found"}), 404 return jsonify({"error": "User not found"}), 404
@ -25,6 +26,28 @@ def user_login():
session = SessionService.create_session(user, org) session = SessionService.create_session(user, org)
return jsonify(session.to_dict()), 201 return jsonify(session.to_dict()), 201
elif session_token := request.headers.get("Authorization"):
session = SessionService.get_session(session_token)
if not session:
return jsonify({"error": "Not authenticated"}), 401
if session.verified:
return jsonify(session.to_dict()), 200
if not "signature" in data:
return jsonify({"error": "Missing required fields"}), 400
signature = data["signature"]
signature = base64.b64decode(signature)
try:
SessionService.verify_session(session_token, signature)
except InvalidSignature:
return jsonify({"error": "Invalid signature"}), 400
return jsonify(session.to_dict()), 200
return jsonify({"error": "Missing required fields"}), 400
@user_bp.route("/logout", methods=["POST"]) @user_bp.route("/logout", methods=["POST"])
def user_logout(): def user_logout():

View File

@ -34,7 +34,8 @@ class OrganizationService:
Perm.ROLE_UP, Perm.ROLE_UP,
Perm.ROLE_MOD Perm.ROLE_MOD
]), ]),
"users": [user.id] "users": [],
"status": "active"
} }
} }
@ -54,7 +55,7 @@ class OrganizationService:
db.refresh(organization) db.refresh(organization)
UserService().add_org_to_user(user, organization) UserService().add_org_to_user(user, organization)
RoleService().add_user_to_role(user, organization, "manager") RoleService().add_user_to_role("manager", organization, user)
UserService().add_public_key_to_user(user, organization, public_key) UserService().add_public_key_to_user(user, organization, public_key)
return organization return organization
@ -97,7 +98,7 @@ class OrganizationService:
if OrganizationService.get_user_status(org, user.id) != "active": if OrganizationService.get_user_status(org, user.id) != "active":
return {"error": "User already suspended"}, 400 return {"error": "User already suspended"}, 400
if user.roles[org.id] == "manager": if user.id in RoleService.get_users_in_role(org, "manager"):
return {"error": "Cannot suspend manager"}, 400 return {"error": "Cannot suspend manager"}, 400
org.users[str(user.id)]["status"] = "suspended" org.users[str(user.id)]["status"] = "suspended"

View File

@ -1,5 +1,8 @@
import secrets import secrets
from cryptography.hazmat.primitives.serialization import load_pem_public_key
from cryptography.hazmat.primitives.asymmetric import padding
from cryptography.hazmat.primitives import hashes
from sqlalchemy.orm.attributes import flag_modified from sqlalchemy.orm.attributes import flag_modified
from database import db from database import db
@ -16,13 +19,35 @@ class SessionService:
user_id=user.id, user_id=user.id,
org_id=org.id, org_id=org.id,
token=secrets.token_hex(128), token=secrets.token_hex(128),
roles=[] roles=[],
challenge=secrets.token_hex(128),
verified=False
) )
db.add(session) db.add(session)
db.commit() db.commit()
db.refresh(session) db.refresh(session)
return session return session
@staticmethod
def verify_session(token: str, signature: bytes):
session = SessionService.get_session(token)
if not session:
raise ValueError(f"Session {token} not found")
public_key_pem = User.query.get(session.user_id).public_keys.get(str(session.org_id))
if not public_key_pem:
raise ValueError(f"Public key not found for user {session.user_id} in organization {session.org_id}")
public_key = load_pem_public_key(public_key_pem.encode())
public_key.verify(
signature,
session.challenge.encode(),
padding.PKCS1v15(),
hashes.SHA256()
)
session.challenge = None
session.verified = True
db.commit()
db.refresh(session)
@staticmethod @staticmethod
def get_session(token: str) -> Session | None: def get_session(token: str) -> Session | None:
return db.query(Session).filter(Session.token == token).first() return db.query(Session).filter(Session.token == token).first()
@ -43,6 +68,9 @@ class SessionService:
if not session: if not session:
return jsonify({"error": "Not authenticated"}), 401 return jsonify({"error": "Not authenticated"}), 401
if not session.verified:
return jsonify({"error": "Session has not yet been verified"}), 403
org = OrganizationService.get_organization(session.org_id) org = OrganizationService.get_organization(session.org_id)
if not org: if not org:
return jsonify({"error": "Organization not found"}), 404 return jsonify({"error": "Organization not found"}), 404
@ -51,10 +79,10 @@ class SessionService:
if status != "active": if status != "active":
return jsonify({"error": "User is not active"}), 403 return jsonify({"error": "User is not active"}), 403
if required_perms: #if required_perms:
for perm in required_perms: # for perm in required_perms:
if not SessionService.check_permission(session, perm, doc_handle): # if not SessionService.check_permission(session, perm, doc_handle):
return jsonify({"error": f"Permission denied, missing required permission: {perm}"}), 403 # return jsonify({"error": f"Permission denied, missing required permission: {perm}"}), 403
return session return session

View File

@ -2,6 +2,7 @@ from sqlalchemy.orm.attributes import flag_modified
from database import db from database import db
from models import User, Organization from models import User, Organization
from utils import encode_public_key
class UserService: class UserService:
@ -13,7 +14,7 @@ class UserService:
full_name=full_name, full_name=full_name,
email=email, email=email,
roles={}, roles={},
public_keys={org.id: public_key} if org else {}, public_keys={org.id: encode_public_key(public_key)} if org else {},
orgs={org.id: { orgs={org.id: {
"name": org.name, "name": org.name,
"status": "active" "status": "active"

View File

@ -1,3 +1,3 @@
from .checks import check_valid_time from .checks import check_valid_time
from .hashing import get_hash, get_hex_from_temp_file from .hashing import get_hash, get_hex_from_temp_file, encode_public_key
from .perms import Perm, PermOperation from .perms import Perm, PermOperation

View File

@ -1,5 +1,6 @@
from tempfile import SpooledTemporaryFile from tempfile import SpooledTemporaryFile
import cryptography.hazmat.primitives.hashes import cryptography.hazmat.primitives.hashes
from cryptography.hazmat.primitives.serialization import Encoding, PublicFormat, load_pem_public_key
def get_hash(data): def get_hash(data):
@ -14,3 +15,13 @@ def get_hex_from_temp_file(temp_file: SpooledTemporaryFile) -> bytes:
temp_file.seek(0) temp_file.seek(0)
file_data = temp_file.read() file_data = temp_file.read()
return file_data return file_data
def encode_public_key(public_key):
if isinstance(public_key, str):
public_key = load_pem_public_key(public_key.encode('utf-8'))
return public_key.public_bytes(
encoding=cryptography.hazmat.primitives.serialization.Encoding.PEM,
format=cryptography.hazmat.primitives.serialization.PublicFormat.SubjectPublicKeyInfo
).decode('utf-8')