diff --git a/container-compose-dev.yml b/container-compose-dev.yml index 9b48fd88..b0307857 100644 --- a/container-compose-dev.yml +++ b/container-compose-dev.yml @@ -17,5 +17,5 @@ services: volumes: - type: bind source: ./kdlp.underground.software - target: /orbit/docs + target: /usr/local/share/orbit/docs read_only: true diff --git a/orbit/.dockerignore b/orbit/.dockerignore deleted file mode 100644 index 1b30444a..00000000 --- a/orbit/.dockerignore +++ /dev/null @@ -1 +0,0 @@ -orbit.db diff --git a/orbit/Containerfile b/orbit/Containerfile index 856ff967..a31ef9ff 100644 --- a/orbit/Containerfile +++ b/orbit/Containerfile @@ -1,24 +1,14 @@ FROM alpine:3.19 AS build RUN apk update && apk upgrade && apk add \ - python3-dev \ - py3-pip \ - build-base \ - libffi-dev \ + py3-peewee \ envsubst \ ; -COPY requirements.txt /requirements.txt -RUN python3 -m venv /radius-venv && \ - source /radius-venv/bin/activate && \ - pip install -r requirements.txt && \ - : - -COPY . /orbit -WORKDIR /orbit +COPY . /usr/local/share/orbit +WORKDIR /usr/local/share/orbit RUN mkdir -p /var/orbit/ && \ - source /radius-venv/bin/activate && \ ./db.py \ : @@ -29,35 +19,31 @@ RUN test -n "$orbit_version_info" || (echo 'version info is not set' && false) & rm config.py.template \ ; -RUN mkdir \ - /var/git \ - /orbit/docs \ - /etc/cgit \ - ; - -COPY --from=orbit_singularity_git_dir . /var/git/singularity -COPY --from=orbit_docs_source . /orbit/docs -COPY --from=orbit_repos_source . /etc/cgit - FROM alpine:3.19 AS orbit RUN apk update && apk upgrade && apk add \ - python3 \ + py3-bcrypt \ + py3-peewee \ + py3-markdown \ + uwsgi-python3 \ + uwsgi-http \ cgit \ ; -COPY --from=build /orbit /orbit -COPY --from=build /radius-venv /radius-venv +WORKDIR /usr/local/share/orbit + +COPY --from=build /usr/local/share/orbit /usr/local/share/orbit +COPY --from=orbit_docs_source . ./docs COPY --from=build /var/orbit /var/orbit -COPY --from=build /var/git /var/git -COPY --from=build /etc/cgit /etc/cgit +COPY --from=orbit_singularity_git_dir . /var/git/singularity +COPY --from=orbit_repos_source . /etc/cgit COPY cgitrc /etc/cgitrc -RUN chown -R 100:100 /orbit /radius-venv /var/orbit /var/git +RUN chown -R 100:100 /var/orbit USER 100:100 EXPOSE 9098 -CMD /bin/sh -c "source /radius-venv/bin/activate && uwsgi /orbit/radius.ini" +CMD ["uwsgi", "--plugin", "python,http", "./radius.ini"] diff --git a/orbit/README.md b/orbit/README.md deleted file mode 100644 index 8ebcc009..00000000 --- a/orbit/README.md +++ /dev/null @@ -1,6 +0,0 @@ -# orbit - -For development, setup and enter a python virtualenv for -(e.g. `python -m venv orbit-dev && source orbit-dev/bin/activate`) -and install the development dependencies: `pip install -r dev-requirements.txt` -to enable style checking via `test-style.sh` utilizing flake8. diff --git a/orbit/config.py b/orbit/config.py index 67b781d1..07fe92a3 100644 --- a/orbit/config.py +++ b/orbit/config.py @@ -1,9 +1,8 @@ version_info = '${orbit_version_info}' # read these documents from a filesystem path -orbit_root = '/orbit' -doc_root = f'{orbit_root}/docs' -doc_header = f'{orbit_root}/header.html' +doc_root = './docs' +doc_header = './header.html' database = '/var/orbit/orbit.db' # duration of authentication token validity period diff --git a/orbit/dev-requirements.txt b/orbit/dev-requirements.txt deleted file mode 100644 index 2c9e2a97..00000000 --- a/orbit/dev-requirements.txt +++ /dev/null @@ -1,4 +0,0 @@ -flake8 ~=7.0.0 -mccabe ~= 0.7.0 -pycodestyle ~= 2.11.1 -pyflakes ~= 3.2.0 diff --git a/orbit/hyperspace.py b/orbit/hyperspace.py index f8f39260..6c750901 100755 --- a/orbit/hyperspace.py +++ b/orbit/hyperspace.py @@ -8,7 +8,6 @@ # internal imports import config -from radius import Session def errx(msg): @@ -16,77 +15,38 @@ def errx(msg): exit(1) -def need(a, u=False, p=False, t=False): +def need(a, u=False, p=False): + needed = [] if u and a.username is None: - errx("Need username. Bye.") + needed.append('username') if p and a.password is None: - errx("Need password. Bye.") - if t and a.token is None: - errx("Need token. Bye.") + needed.append('password') + if needed: + errx(f"Need {' and '.join(needed)}. Bye.") def nou(u): errx(f'no such user "{u}". Bye.') -def do_query_username(args): - need(args, u=True) - if not (user := db.User.get_or_none(db.User.username == args.username)): - nou(args.username) - print(f'Username : {user.username}\n' - f'Hashed Password : {user.pwdhash}\n' - f'Student ID : {user.student_id}') - - -def do_validate_token(args): - need(args, t=True) - - ses = db.Session.get_or_none(db.Session.token == args.token) - if ses: - print(ses.username) - else: - print('null') - - def do_drop_session(args): need(args, u=True) query = (db.Session .delete() - .where(db.Session.username == args.username) - .returning(db.Session)) - - if ses := next(iter(query.execute()), None): - print(ses.username) - else: - print('null') - - -def do_create_session(args): - need(args, u=True) - ses = Session(username=args.username) - print(ses.token) - + .where(db.Session.username == args.username)) -def do_validate_creds(args): - need(args, u=True, p=True) - if not (user := db.User.get_or_none(db.User.username == args.username)): - nou(args.username) - if not bcrypt.checkpw(args.password.encode('utf-8'), - user.pwdhash.encode('utf-8')): - print('null') - return - print(f'credentials(username: {args.username}, password:{args.password})') + if query.execute() < 1: + errx('No session belonging to that user found') def do_change_password(args): need(args, u=True, p=True) - new_hash = do_bcrypt_hash(args, get=True) + new_hash = do_bcrypt_hash(args) query = (db.User .update({db.User.pwdhash: new_hash}) .where(db.User.username == args.username)) if query.execute() < 1: nou(args.username) - print(f'credentials(username: {args.username}, password:{args.password})') def do_delete_user(args): @@ -96,22 +56,17 @@ def do_delete_user(args): .where(db.User.username == args.username)) if query.execute() < 1: nou(args.username) - print(args.username) -def do_bcrypt_hash(args, get=False): +def do_bcrypt_hash(args): need(args, p=True) - res = str(bcrypt.hashpw(bytes(args.password, "UTF-8"), - bcrypt.gensalt()), "UTF-8") - if get: - return res - else: - print(res) + return bcrypt.hashpw(args.password.encode('utf-8'), + bcrypt.gensalt()).decode('utf-8') def do_newuser(args): need(args, u=True, p=True) - new_hash = do_bcrypt_hash(args, get=True) + new_hash = do_bcrypt_hash(args) try: db.User.create(username=args.username, pwdhash=new_hash, student_id=args.studentid) @@ -119,7 +74,6 @@ def do_newuser(args): db.Registration.create(username=args.username, password=args.password, student_id=args.studentid) - do_validate_creds(args) except db.peewee.IntegrityError as e: errx(f'cannot create user with duplicate field: "{e}"') @@ -145,9 +99,6 @@ def hyperspace_main(raw_args): parser.add_argument('-u', '--username', help='Username to operate with') parser.add_argument('-p', '--password', help='Password to operate with') parser.add_argument('-i', '--studentid', help='Student ID to operate with') - parser.add_argument('-t', '--token', help='Token to operate with') - parser.add_argument('-e', '--exercise', - help='Assignment/Exercise to operate with') actions = parser.add_mutually_exclusive_group() actions.add_argument('-r', '--roster', action='store_const', @@ -156,39 +107,24 @@ def hyperspace_main(raw_args): actions.add_argument('-n', '--newuser', action='store_const', help='Create a new user from supplied credentials', dest='do', const=do_newuser) - actions.add_argument('-s', '--session', action='store_const', - help='Check valitity of supplied token', - dest='do', const=do_validate_token) - actions.add_argument('-d', '--dropsession', action='store_const', - help='Drop any existing valid session for supplied username', # NOQA: E501 - dest='do', const=do_drop_session) - actions.add_argument('-c', '--createsession', action='store_const', - help='Create session for supplied username', - dest='do', const=do_create_session) - actions.add_argument('-v', '--validatecreds', action='store_const', - help='Create session for supplied username', - dest='do', const=do_validate_creds) actions.add_argument('-m', '--mutatepassword', action='store_const', help='Change password for supplied username to supplied password', # NOQA: E501 dest='do', const=do_change_password) actions.add_argument('-w', '--withdrawuser', action='store_const', help='Delete ("withdraw") the supplied username', dest='do', const=do_delete_user) - actions.add_argument('-b', '--bcrypthash', action='store_const', - help='Generate bcrypt hash from supplied password', - dest='do', const=do_bcrypt_hash) actions.add_argument('-l', '--listsessions', action='store_const', help='List of all known sessions (some could be invalid)', # NOQA: E501 dest='do', const=do_list_sessions) - actions.add_argument('-q', '--queryuser', action='store_const', - help='Get information about supplied username if valid', # NOQA: E501 - dest='do', const=do_query_username) + actions.add_argument('-d', '--dropsession', action='store_const', + help='Drop any existing valid session for supplied username', # NOQA: E501 + dest='do', const=do_drop_session) args = parser.parse_args(raw_args) if (args.do): args.do(args) else: - print("Nothing to do. Tip: -h") + parser.print_help() if __name__ == "__main__": diff --git a/orbit/radius.ini b/orbit/radius.ini index bb303375..5d4696c0 100644 --- a/orbit/radius.ini +++ b/orbit/radius.ini @@ -1,6 +1,5 @@ [uwsgi] http = 0.0.0.0:9098 -chdir = /orbit wsgi-file = radius.py disable-logging diff --git a/orbit/radius.py b/orbit/radius.py index 5c54cb8f..1de2f79b 100644 --- a/orbit/radius.py +++ b/orbit/radius.py @@ -3,13 +3,13 @@ # it's all one things now import bcrypt -import hashlib import html import markdown import os import re import subprocess import sys +import secrets from http import HTTPStatus, cookies from datetime import datetime, timedelta from urllib.parse import parse_qs, urlparse @@ -27,34 +27,10 @@ # === utilities === -def encode(dat): return bytes(dat, "UTF-8") -def decode(dat): return str(dat, "UTF-8") - - -def mk_table(row_list, indentation_level=0): - # Create elements in first row, and elements afterwards - first_row = True - def indenter(adjustment): return '\t' * (indentation_level + adjustment) - - output = f'{indenter(0)}' - for row in row_list: - output += f'{indenter(1)}' - for column in row: - if first_row: - output += f'{indenter(2)}' - first_row = False - else: - output += f'{indenter(2)}' - output += f'{indenter(1)}' - output += f'{indenter(0)}
{column}{column}
' - - return output - - def check_credentials(username, password): if not (user := db.User.get_or_none(db.User.username == username)): return False - return bcrypt.checkpw(encode(password), encode(user.pwdhash)) + return bcrypt.checkpw(password.encode(), user.pwdhash.encode()) # === user session handling === @@ -141,8 +117,7 @@ def valid(self): return self.token def mk_hash(self, username): - hash_input = username + str(datetime.now()) - return hashlib.sha256(encode(hash_input)).hexdigest() + return secrets.token_hex() def expired(self): if (expiry := self.expiry) is None or datetime.utcnow() > expiry: @@ -166,12 +141,6 @@ def mk_cookie_header(self): return [('Set-Cookie', cookie_val)] - def __repr__(self): - return f'Session({self.token},{self.username},{self.expiry})' - - def __str__(self): - return repr(self) - class Rocket: """ @@ -226,16 +195,6 @@ def __init__(self, env, start_response): self.headers = [] self.body_args = self.read_body_args_wsgi() - def __repr__(self): - return ( - f'Rocket({self.method},{self.path_info},{self.queries},' - f'{str(self.headers)},{self._msg},{str(self.session)},' - f'{self.body_args})' - ) - - def __str__(self): - return repr(self) - def msg(self, msg): self._msg = msg @@ -264,19 +223,9 @@ def username(self): if session := self.session: return session.username - @property - def token(self): - if session := self.session: - return session.token - - @property - def expiry(self): - if session := self.session: - return session.expiry - def body_args_query(self, key): return html.escape( - decode(self.body_args.get(encode(key), [b''])[0])) + self.body_args.get(key.encode(), [b''])[0].decode()) def queries_query(self, key): return self.queries.get(key, [''])[0] @@ -320,31 +269,38 @@ def raw_respond(self, response_code, body=b''): def respond(self, response_document): self.headers += [('Content-Type', 'text/html')] response_document = self.format_html(response_document) - return self.raw_respond(HTTPStatus.OK, encode(response_document)) + return self.raw_respond(HTTPStatus.OK, response_document.encode()) -form_welcome_template = """ +def mk_form_welcome(session): + return f'''
- {} + + + + + +
Cookie KeyValue
Token{session.token}
User{session.username}
Expiry{session.expiry_fmt()}
Welcome!
- {} -
-""".strip() +
+ +
+ ''' -form_welcome_buttons = """ -
- -
-""".strip() # NOQA: E501 -form_login = """ -
+def login_form(target_location=None): + if target_location is not None: + target_redir = f'?target={target_location}' + else: + target_redir = '' + return f''' +
@@ -353,22 +309,7 @@ def respond(self, response_document):
-

Need an account? Register here


-""".strip() - - -def cookie_info_table(session): - return mk_table([ - ('Cookie Key', 'Value'), - ('Token', session.token), - ('User', session.username), - ('Expiry', session.expiry_fmt()), - ('Remaining Validity', str(session.expiry - datetime.utcnow()))]) - - -def mk_form_welcome(session): - return form_welcome_template.format(cookie_info_table(session), - form_welcome_buttons) +

Need an account? Register here


''' def handle_login(rocket): @@ -384,12 +325,11 @@ def respond(welcome): rocket.headers += [('Location', target)] return rocket.raw_respond(HTTPStatus.SEE_OTHER) elif target: - return rocket.respond(form_login % ({'target_redir': - f'?target={target}'})) + return rocket.respond(login_form(target_location=target)) elif welcome: return rocket.respond(mk_form_welcome(rocket.session)) else: - return rocket.respond(form_login % ({'target_redir': ''})) + return rocket.respond(login_form()) if rocket.session: rocket.msg(f'{rocket.username} authenticated by token') @@ -442,25 +382,15 @@ def handle_dashboard(rocket): return handle_stub(rocket, ['dashboard in development, check back later']) -form_register = """ +def handle_register(rocket): + def form_respond(): + return rocket.respond('''

-
-""".strip() - + ''') -register_response = """ -

Save these credentials, you will not be able to access them again


-

Username: %(username)s


-

Password: %(password)s


-""".strip() - - -def handle_register(rocket): - def form_respond(): - return rocket.respond(form_register) if rocket.method != 'POST': return form_respond() if not (student_id := rocket.body_args_query('student_id')): @@ -474,10 +404,10 @@ def form_respond(): rocket.msg('no such student') return form_respond() rocket.msg('welcome to the classroom') - return rocket.respond((register_response % { - 'username': registration.username, - 'password': registration.password, - })) + return rocket.respond(f''' +

Save these credentials, you will not be able to access them again


+

Username: {registration.username}


+

Password: {registration.password}


''') def handle_cgit(rocket): @@ -497,7 +427,7 @@ def handle_cgit(rocket): env=cgit_env) so, se = proc.communicate() try: - outstring = str(so, 'UTF-8') + outstring = so.decode() begin = outstring.index('\n\n') return rocket.respond(outstring[begin+2:]) except (UnicodeDecodeError, ValueError) as ex: diff --git a/orbit/requirements.txt b/orbit/requirements.txt deleted file mode 100644 index a1543266..00000000 --- a/orbit/requirements.txt +++ /dev/null @@ -1,7 +0,0 @@ -bcrypt ~= 3.1.6 -certbot ~= 2.6.0 -Markdown ~= 3.3.7 -urllib3 ~= 1.26.16 -uWSGI ~= 2.0.21 -uwsgi-tools ~= 1.1.1 -peewee ~= 3.17.1 diff --git a/orbit/warpdrive.sh b/orbit/warpdrive.sh index ed4ef082..19344730 100755 --- a/orbit/warpdrive.sh +++ b/orbit/warpdrive.sh @@ -12,7 +12,4 @@ require "${DOCKER}" CONTAINER=${CONTAINER:-singularity_orbit_1} -cat <