Skip to content

Commit 8fa7f3d

Browse files
authored
Manage CTFd configuration from ctfcli (#155)
* Add instance command with config subcommand * Use logging * Add instance folder * Fix typing
1 parent 704a864 commit 8fa7f3d

File tree

6 files changed

+143
-1
lines changed

6 files changed

+143
-1
lines changed

.gitignore

-1
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,6 @@ db.sqlite3
6262
db.sqlite3-journal
6363

6464
# Flask stuff:
65-
instance/
6665
.webassets-cache
6766

6867
# Scrapy stuff:

ctfcli/__main__.py

+5
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111

1212
from ctfcli.cli.challenges import ChallengeCommand
1313
from ctfcli.cli.config import ConfigCommand
14+
from ctfcli.cli.instance import InstanceCommand
1415
from ctfcli.cli.pages import PagesCommand
1516
from ctfcli.cli.plugins import PluginsCommand
1617
from ctfcli.cli.templates import TemplatesCommand
@@ -101,6 +102,9 @@ def init(
101102
def config(self):
102103
return COMMANDS.get("config")
103104

105+
def instance(self):
106+
return COMMANDS.get("instance")
107+
104108
def challenge(self):
105109
return COMMANDS.get("challenge")
106110

@@ -120,6 +124,7 @@ def templates(self):
120124
"pages": PagesCommand(),
121125
"plugins": PluginsCommand(),
122126
"templates": TemplatesCommand(),
127+
"instance": InstanceCommand(),
123128
"cli": CTFCLI(),
124129
}
125130

ctfcli/cli/instance.py

+69
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import logging
2+
3+
import click
4+
5+
from ctfcli.core.config import Config
6+
from ctfcli.core.instance.config import ServerConfig
7+
8+
log = logging.getLogger("ctfcli.cli.instance")
9+
10+
11+
class ConfigCommand:
12+
def get(self, key):
13+
"""Get the value of a specific remote instance config key"""
14+
log.debug(f"ConfigCommand.get: ({key=})")
15+
return ServerConfig.get(key=key)
16+
17+
def set(self, key, value):
18+
"""Set the value of a specific remote instance config key"""
19+
log.debug(f"ConfigCommand.set: ({key=})")
20+
ServerConfig.set(key=key, value=value)
21+
click.secho(f"Successfully set '{key}' to '{value}'", fg="green")
22+
23+
def pull(self):
24+
"""Copy remote instance configuration values to local config"""
25+
log.debug("ConfigCommand.pull")
26+
server_configs = ServerConfig.getall()
27+
28+
config = Config()
29+
if config.config.has_section("instance") is False:
30+
config.config.add_section("instance")
31+
32+
for k, v in server_configs.items():
33+
# We always store as a string because the CTFd Configs model is a string
34+
if v == "None":
35+
v = "null"
36+
config.config.set("instance", k, str(v))
37+
38+
with open(config.config_path, "w+") as f:
39+
config.write(f)
40+
41+
click.secho("Successfully pulled configuration", fg="green")
42+
43+
def push(self):
44+
"""Save local instance configuration values to remote CTFd instance"""
45+
log.debug("ConfigCommand.push")
46+
config = Config()
47+
if config.config.has_section("instance") is False:
48+
config.config.add_section("instance")
49+
50+
configs = {}
51+
for k in config["instance"]:
52+
v = config["instance"][k]
53+
if v == "null":
54+
v = None
55+
configs[k] = v
56+
57+
failed_configs = ServerConfig.setall(configs=configs)
58+
for f in failed_configs:
59+
click.secho(f"Failed to push config {f}", fg="red")
60+
61+
if not failed_configs:
62+
click.secho("Successfully pushed config", fg="green")
63+
else:
64+
return 1
65+
66+
67+
class InstanceCommand:
68+
def config(self):
69+
return ConfigCommand

ctfcli/core/exceptions.py

+4
Original file line numberDiff line numberDiff line change
@@ -55,3 +55,7 @@ class InvalidPageConfiguration(PageException):
5555

5656
class IllegalPageOperation(PageException):
5757
pass
58+
59+
60+
class InstanceConfigException(Exception):
61+
pass

ctfcli/core/instance/__init__.py

Whitespace-only changes.

ctfcli/core/instance/config.py

+65
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
from typing import List
2+
3+
from ctfcli.core.api import API
4+
from ctfcli.core.exceptions import InstanceConfigException
5+
6+
7+
class ServerConfig:
8+
@staticmethod
9+
def get(key: str) -> str:
10+
api = API()
11+
resp = api.get(f"/api/v1/configs/{key}")
12+
if resp.ok is False:
13+
raise InstanceConfigException(
14+
f"Could not get config {key=} because '{resp.content}' with {resp.status_code}"
15+
)
16+
resp = resp.json()
17+
return resp["data"]["value"]
18+
19+
@staticmethod
20+
def set(key: str, value: str) -> bool:
21+
api = API()
22+
data = {
23+
"value": value,
24+
}
25+
resp = api.patch(f"/api/v1/configs/{key}", json=data)
26+
if resp.ok is False:
27+
raise InstanceConfigException(
28+
f"Could not get config {key=} because '{resp.content}' with {resp.status_code}"
29+
)
30+
resp = resp.json()
31+
32+
return resp["success"]
33+
34+
@staticmethod
35+
def getall():
36+
api = API()
37+
resp = api.get("/api/v1/configs")
38+
if resp.ok is False:
39+
raise InstanceConfigException(f"Could not get configs because '{resp.content}' with {resp.status_code}")
40+
resp = resp.json()
41+
configs = resp["data"]
42+
43+
config = {}
44+
for c in configs:
45+
# Ignore alembic_version configs as they are managed by plugins
46+
if c["key"].endswith("alembic_version") is False:
47+
config[c["key"]] = c["value"]
48+
49+
# Not much point in saving internal configs
50+
del config["ctf_version"]
51+
del config["version_latest"]
52+
del config["next_update_check"]
53+
del config["setup"]
54+
55+
return config
56+
57+
@staticmethod
58+
def setall(configs) -> List[str]:
59+
failed = []
60+
for k, v in configs.items():
61+
try:
62+
ServerConfig.set(key=k, value=v)
63+
except InstanceConfigException:
64+
failed.append(k)
65+
return failed

0 commit comments

Comments
 (0)