Skip to content

Commit

Permalink
Game levels (#447)
Browse files Browse the repository at this point in the history
Adds additional options to the game levels and management. You can now choose to unlock a level via points reached or set it to "managed" where you control who is added to a level. A new button "Edit Access" is on the level which allows you to add and remove users. You can now set the type on level 0, so you can change it from default to prevent new users from automatically getting the level. Note that managed levels are hidden from users until they are added to the level.
  • Loading branch information
eljeffeg authored Jul 1, 2021
1 parent 6b77f8c commit 2f52b90
Show file tree
Hide file tree
Showing 36 changed files with 507 additions and 161 deletions.
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ RUN mkdir /opt/rtb
ADD . /opt/rtb

RUN apt-get update
RUN apt-get install build-essential zlib1g-dev -y
RUN apt-get install build-essential zlib1g-dev rustc -y
RUN apt-get install python3-pycurl sqlite3 libsqlite3-dev -y

ADD ./setup/requirements.txt ./
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ See the [Root the Box Wiki](https://github.com/moloch--/RootTheBox/wiki)
## Platform Requirements

- [Python 2.7.x or <= 3.8.x](https://www.python.org/), [PyPy](http://pypy.org/) or [Docker](https://github.com/moloch--/RootTheBox/wiki/Docker-Deployment). (*Note: Python 3.9 breaks thigns as it removes Py2/3 compatibility.*)
- Install scripts are for [Ubuntu](http://www.ubuntu.com/) >= 18.04 (or [Debian](https://www.debian.org/)) but the application should work on any recent Linux, BSD, or OSX system.
- Install scripts are for [Ubuntu](http://www.ubuntu.com/) >= 18.04 (or [Debian](https://www.debian.org/)) but the application should work on any recent Linux, BSD, MacOS, or Windows system.

## Questions? Problems? Feature Requests?

Expand Down
57 changes: 55 additions & 2 deletions handlers/AdminHandlers/AdminGameObjectHandlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@
)
from libs.ValidationError import ValidationError
from libs.SecurityDecorators import *
from libs.StringCoding import decode
from builtins import str


Expand Down Expand Up @@ -489,7 +490,7 @@ class AdminEditHandler(BaseHandler):
@authenticated
@authorized(ADMIN_PERMISSION)
def get(self, *args, **kwargs):
""" Just redirect to the corisponding /view page """
""" Just redirect to the corresponding /view page """
uri = {
"corporation": "game_objects",
"box": "game_objects",
Expand Down Expand Up @@ -523,6 +524,7 @@ def post(self, *args, **kwargs):
"market_item": self.edit_market_item,
"category": self.edit_category,
"flag_order": self.edit_flag_order,
"level_access": self.edit_level_access,
}
if len(args) and args[0] in uri:
uri[args[0]]()
Expand Down Expand Up @@ -803,6 +805,43 @@ def edit_ip(self):
"admin/view/game_objects.html", success=None, errors=[str(error)]
)

def edit_level_access(self):
""" Update game level access """
try:
level = GameLevel.by_uuid(self.get_argument("uuid", ""))
if level is None:
raise ValidationError("Game level does not exist")
else:
teams = []
lv_teams = level.teams
for team in lv_teams:
teams.append(team.uuid)
access = self.request.arguments.get("accessList", [])
available = self.request.arguments.get("availableList", [])
if not isinstance(access, list):
access = [access]
if not isinstance(available, list):
available = [available]
for team_uuid in access:
if decode(team_uuid) not in teams:
team = Team.by_uuid(team_uuid)
if team:
team.game_levels.append(level)
self.dbsession.add(team)
self.dbsession.commit()
for team_uuid in available:
if decode(team_uuid) in teams:
team = Team.by_uuid(team_uuid)
if team:
team.game_levels.remove(level)
self.dbsession.add(team)
self.dbsession.commit()
self.redirect("/admin/view/game_levels")
except ValueError:
raise ValidationError("That was not a number ...")
except ValidationError as error:
self.render("admin/view/game_levels.html", errors=[str(error)])

def edit_game_level(self):
""" Update game level objects """
try:
Expand All @@ -820,7 +859,7 @@ def edit_game_level(self):
level.buyout = min(level.buyout, 100)
elif level._type == "none":
level.buyout = 0
if level._type != "none" and level.buyout == 0:
if level._type != "none" and level._type != "hidden" and level.buyout == 0:
level._type = "none"
self.dbsession.add(level)
self.dbsession.flush()
Expand Down Expand Up @@ -1107,6 +1146,20 @@ def post(self, *args, **kwargs):
self.write(obj)
else:
self.write({"Error": "Invalid uuid."})
elif obj_name == "access":
obj = game_objects["game_level"].by_uuid(uuid)
if obj is not None:
all_teams = Team.all()
access = []
available = []
for team in obj.teams:
all_teams.remove(team)
access.append(team.to_dict())
for team in all_teams:
available.append(team.to_dict())
self.write({"available": available, "access": access})
else:
self.write({"Error": "Invalid uuid."})
else:
self.write({"Error": "Invalid object type."})
self.finish()
Expand Down
25 changes: 24 additions & 1 deletion handlers/MissionsHandler.py
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,14 @@ def get(self, *args, **kwargs):
box = Box.by_uuid(uuid)
if box is not None:
user = self.get_current_user()
if box.locked:
level = GameLevel.by_id(box.game_level_id)
if (
user.team
and level.type != "none"
and level not in user.team.game_levels
):
self.redirect("/403")
elif box.locked:
self.render(
"missions/status.html",
errors=None,
Expand Down Expand Up @@ -339,6 +346,22 @@ def success_capture(self, flag, old_reward=None):
# Fire level complete webhook
send_level_complete_webhook(user, box)

# Unlock level if based on Game Score
for lv in GameLevel.all():
if (
lv.type == "points"
and lv.buyout <= user.team.money
and lv not in user.team.game_levels
):
logging.info(
"%s (%s) unlocked %s" % (user.handle, user.team.name, lv.name)
)
user.team.game_levels.append(lv)
self.dbsession.add(user.team)
self.dbsession.commit()
self.event_manager.level_unlocked(user, lv)
success.append("Congratulations! You have unlocked " + lv.name)

# Unlock next level if based on Game Progress
next_level = GameLevel.by_id(level.next_level_id)
if next_level and next_level not in user.team.game_levels:
Expand Down
43 changes: 26 additions & 17 deletions handlers/PublicHandlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@
from tornado.options import options
from msal import ConfidentialClientApplication


class HomePageHandler(BaseHandler):
def get(self, *args, **kwargs):
""" Renders the main page """
Expand All @@ -76,10 +77,7 @@ def get(self, *args, **kwargs):
code = self.get_argument("code")
code_flow = self.memcached.get(state)
self.memcached.delete(state)
args = {
"code": code,
"state": state
}
args = {"code": code, "state": state}
result = azuread_app.acquire_token_by_auth_code_flow(code_flow, args)
if "error" in result:
self.redirect("/403")
Expand All @@ -91,7 +89,7 @@ def get(self, *args, **kwargs):
hasAdminRole = self.is_admin(claims)

# Get the team code (if set) would have come from the Join Team page.
team_code = None;
team_code = None
if "teamcode" in code_flow:
team_code = code_flow["teamcode"]

Expand Down Expand Up @@ -163,7 +161,10 @@ def add_user(self, claims, team_code):
user.uuid = claims["oid"]
user.handle = claims["preferred_username"].split("@")[0]
# Generate a long random password that the user will never know or use.
user.password = ''.join(secrets.choice(string.ascii_letters + string.digits + string.punctuation) for i in range(30))
user.password = "".join(
secrets.choice(string.ascii_letters + string.digits + string.punctuation)
for i in range(30)
)
user.bank_password = False
user.name = claims["name"]
user.email = claims["email"]
Expand All @@ -176,14 +177,14 @@ def add_user(self, claims, team_code):
return user

def update_permissions(self, user, isAdmin):
# Update permissions, in-case the user has been added to or removed from the
# Update permissions, in-case the user has been added to or removed from the
# admin role in Azure AD.
if isAdmin and not user.is_admin():
permission = Permission()
permission.name = ADMIN_PERMISSION
permission.user_id = user.id
self.dbsession.add(permission)
user.team_id = None # Admins aren't part of a team.
user.team_id = None # Admins aren't part of a team.
elif not isAdmin and user.is_admin():
permissions = Permission.by_user_id(user.id)
for permission in permissions:
Expand Down Expand Up @@ -227,7 +228,9 @@ def post(self, *args, **kwargs):
self.failed_login()

def build_auth_code_flow(self):
codeflow = azuread_app.initiate_auth_code_flow(["email"], redirect_uri=options.redirect_url)
codeflow = azuread_app.initiate_auth_code_flow(
["email"], redirect_uri=options.redirect_url
)
return codeflow

def allowed_ip(self):
Expand Down Expand Up @@ -401,7 +404,10 @@ def form_validation(self):
is not None
):
raise ValidationError("This handle is already registered")
if options.require_email and User.by_email(self.get_argument("email", None)) is not None:
if (
options.require_email
and User.by_email(self.get_argument("email", None)) is not None
):
raise ValidationError("This email address is already registered")
if self.get_argument("pass1", "") != self.get_argument("pass2", ""):
raise ValidationError("Passwords do not match")
Expand Down Expand Up @@ -493,10 +499,12 @@ def create_team(self):
team.money = self.config.starting_team_money
else:
team.money = 0
level_0 = GameLevel.by_number(0)
if not level_0:
level_0 = GameLevel.all()[0]
team.game_levels.append(level_0)
levels = GameLevel.all()
for level in levels:
if level.type == "none":
team.game_levels.append(level)
elif level.type != "hidden" and level.buyout == 0:
team.game_levels.append(level)
return team
elif self.config.public_teams:
if Team.by_name(self.get_argument("team_name", "")) is not None:
Expand Down Expand Up @@ -632,9 +640,9 @@ def post(self, *args, **kwargs):
login_hint = self.get_argument("login-hint", None)
if len(login_hint) == 0:
login_hint = None
code_flow = azuread_app.initiate_auth_code_flow(["email"],
redirect_uri=options.redirect_url,
login_hint=login_hint)
code_flow = azuread_app.initiate_auth_code_flow(
["email"], redirect_uri=options.redirect_url, login_hint=login_hint
)
code_flow["teamcode"] = code
self.memcached.add(code_flow["state"], code_flow)
self.redirect(code_flow["auth_uri"])
Expand All @@ -657,6 +665,7 @@ def validate_teamcode(self, teamcode):
raise ValidationError("Team %s is already full" % team.name)
return teamcode


class FakeRobotsHandler(BaseHandler):
def get(self, *args, **kwargs):
"""
Expand Down
2 changes: 1 addition & 1 deletion handlers/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -257,7 +257,7 @@ def get_cookie_secret():
scoreboard_state={},
# Application version
version=__version__,
autoreload=options.autoreload_source
autoreload=options.autoreload_source,
)


Expand Down
9 changes: 8 additions & 1 deletion libs/DatabaseConnection.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,14 @@

class DatabaseConnection(object):
def __init__(
self, database, hostname="", port="", username="", password="", dialect="", ssl_ca=""
self,
database,
hostname="",
port="",
username="",
password="",
dialect="",
ssl_ca="",
):
self.database = database
self.hostname = hostname
Expand Down
27 changes: 19 additions & 8 deletions libs/Identicon.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,19 +24,30 @@ def _rect(x, y, width, height, color, stroke, stroke_weight):
)


def identicon(str_, size, background="#f0f0f0"):
def identicon(str_, size, background="#f0f0f0", square=False):
digest = int(md5(str_.encode("utf-8")).hexdigest(), 16)
color = "#{:06x}".format(digest & 0xFFFFFF)
stroke_weight = 0.02
digest, body = digest >> 24, ""
x = y = 0
for t in range(size ** 2):
if digest & 1:
body += _rect(x, y, 1, 1, color, background, stroke_weight)
body += _rect(size * 2 - x - 1, y, 1, 1, color, background, stroke_weight)
digest, y = digest >> 1, y + 1
x, y = (x + 1, 0) if y == size else (x, y)
image_data = _svg(size * 2, size, body, background)
if square:
for t in range(size ** 2 // 2):
if digest & 1:
body += _rect(x, y, 1, 1, color, background, stroke_weight)
body += _rect(size - x - 1, y, 1, 1, color, background, stroke_weight)
digest, y = digest >> 1, y + 1
x, y = (x + 1, 0) if y == size else (x, y)
image_data = _svg(size, size, body, background)
else:
for t in range(size ** 2):
if digest & 1:
body += _rect(x, y, 1, 1, color, background, stroke_weight)
body += _rect(
size * 2 - x - 1, y, 1, 1, color, background, stroke_weight
)
digest, y = digest >> 1, y + 1
x, y = (x + 1, 0) if y == size else (x, y)
image_data = _svg(size * 2, size, body, background)
avatar = "upload/%s.svg" % str(digest)
file_path = path.join(options.avatar_dir, avatar)
with open(file_path, "w") as fp:
Expand Down
Loading

0 comments on commit 2f52b90

Please sign in to comment.