mirror of https://github.com/veops/cmdb.git
feat: add inner password storage and optimize flask command about inner cmdb (#248)
Co-authored-by: fxiang21 <fxiang21@126.com>
This commit is contained in:
parent
c9f0de9838
commit
5b314aa907
|
@ -318,6 +318,17 @@ def cmdb_index_table_upgrade():
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
|
|
||||||
|
def valid_address(address):
|
||||||
|
if not address.startswith(("http://127.0.0.1", "https://127.0.0.1")):
|
||||||
|
response = {
|
||||||
|
"message": "Address should start with http://127.0.0.1 or https://127.0.0.1",
|
||||||
|
"status": "failed"
|
||||||
|
}
|
||||||
|
KeyManage.print_response(response)
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
@click.command()
|
@click.command()
|
||||||
@click.option(
|
@click.option(
|
||||||
'-a',
|
'-a',
|
||||||
|
@ -329,15 +340,26 @@ def cmdb_inner_secrets_init(address):
|
||||||
"""
|
"""
|
||||||
init inner secrets for password feature
|
init inner secrets for password feature
|
||||||
"""
|
"""
|
||||||
KeyManage(backend=InnerKVManger).init()
|
res, ok = KeyManage(backend=InnerKVManger).init()
|
||||||
|
if not ok:
|
||||||
|
if res.get("status") == "failed":
|
||||||
|
KeyManage.print_response(res)
|
||||||
|
return
|
||||||
|
|
||||||
if address and address.startswith("http") and current_app.config.get("INNER_TRIGGER_TOKEN", "") != "":
|
token = res.get("details", {}).get("root_token", "")
|
||||||
|
if valid_address(address):
|
||||||
|
token = current_app.config.get("INNER_TRIGGER_TOKEN", "") if not token else token
|
||||||
|
if not token:
|
||||||
|
token = click.prompt(f'Enter root token', hide_input=True, confirmation_prompt=False)
|
||||||
|
assert token is not None
|
||||||
resp = requests.post("{}/api/v0.1/secrets/auto_seal".format(address.strip("/")),
|
resp = requests.post("{}/api/v0.1/secrets/auto_seal".format(address.strip("/")),
|
||||||
headers={"Inner-Token": current_app.config.get("INNER_TRIGGER_TOKEN", "")})
|
headers={"Inner-Token": token})
|
||||||
if resp.status_code == 200:
|
if resp.status_code == 200:
|
||||||
KeyManage.print_response(resp.json())
|
KeyManage.print_response(resp.json())
|
||||||
else:
|
else:
|
||||||
KeyManage.print_response({"message": resp.text, "status": "failed"})
|
KeyManage.print_response({"message": resp.text or resp.status_code, "status": "failed"})
|
||||||
|
else:
|
||||||
|
KeyManage.print_response(res)
|
||||||
|
|
||||||
|
|
||||||
@click.command()
|
@click.command()
|
||||||
|
@ -352,18 +374,19 @@ def cmdb_inner_secrets_unseal(address):
|
||||||
"""
|
"""
|
||||||
unseal the secrets feature
|
unseal the secrets feature
|
||||||
"""
|
"""
|
||||||
address = "{}/api/v0.1/secrets/unseal".format(address.strip("/"))
|
if not valid_address(address):
|
||||||
if not address.startswith("http"):
|
|
||||||
KeyManage.print_response({"message": "invalid address, should start with http", "status": "failed"})
|
|
||||||
return
|
return
|
||||||
|
address = "{}/api/v0.1/secrets/unseal".format(address.strip("/"))
|
||||||
for i in range(global_key_threshold):
|
for i in range(global_key_threshold):
|
||||||
token = click.prompt(f'Enter unseal token {i + 1}', hide_input=True, confirmation_prompt=False)
|
token = click.prompt(f'Enter unseal token {i + 1}', hide_input=True, confirmation_prompt=False)
|
||||||
assert token is not None
|
assert token is not None
|
||||||
resp = requests.post(address, headers={"Unseal-Token": token})
|
resp = requests.post(address, headers={"Unseal-Token": token})
|
||||||
if resp.status_code == 200:
|
if resp.status_code == 200:
|
||||||
KeyManage.print_response(resp.json())
|
KeyManage.print_response(resp.json())
|
||||||
|
if resp.json().get("status") in ["success", "skip"]:
|
||||||
|
return
|
||||||
else:
|
else:
|
||||||
KeyManage.print_response({"message": resp.text, "status": "failed"})
|
KeyManage.print_response({"message": resp.status_code, "status": "failed"})
|
||||||
return
|
return
|
||||||
|
|
||||||
|
|
||||||
|
@ -388,15 +411,16 @@ def cmdb_inner_secrets_seal(address, token):
|
||||||
"""
|
"""
|
||||||
assert address is not None
|
assert address is not None
|
||||||
assert token is not None
|
assert token is not None
|
||||||
if address.startswith("http"):
|
if not valid_address(address):
|
||||||
address = "{}/api/v0.1/secrets/seal".format(address.strip("/"))
|
return
|
||||||
resp = requests.post(address, headers={
|
address = "{}/api/v0.1/secrets/seal".format(address.strip("/"))
|
||||||
"Inner-Token": token,
|
resp = requests.post(address, headers={
|
||||||
})
|
"Inner-Token": token,
|
||||||
if resp.status_code == 200:
|
})
|
||||||
KeyManage.print_response(resp.json())
|
if resp.status_code == 200:
|
||||||
else:
|
KeyManage.print_response(resp.json())
|
||||||
KeyManage.print_response({"message": resp.text, "status": "failed"})
|
else:
|
||||||
|
KeyManage.print_response({"message": resp.status_code, "status": "failed"})
|
||||||
|
|
||||||
|
|
||||||
@click.command()
|
@click.command()
|
||||||
|
|
|
@ -9,12 +9,10 @@ from flask_login import LoginManager
|
||||||
from flask_migrate import Migrate
|
from flask_migrate import Migrate
|
||||||
from flask_sqlalchemy import SQLAlchemy
|
from flask_sqlalchemy import SQLAlchemy
|
||||||
|
|
||||||
|
from api.lib.secrets.inner import KeyManage
|
||||||
from api.lib.utils import ESHandler
|
from api.lib.utils import ESHandler
|
||||||
from api.lib.utils import RedisHandler
|
from api.lib.utils import RedisHandler
|
||||||
|
|
||||||
from api.lib.secrets.inner import KeyManage
|
|
||||||
|
|
||||||
|
|
||||||
bcrypt = Bcrypt()
|
bcrypt = Bcrypt()
|
||||||
login_manager = LoginManager()
|
login_manager = LoginManager()
|
||||||
db = SQLAlchemy(session_options={"autoflush": False})
|
db = SQLAlchemy(session_options={"autoflush": False})
|
||||||
|
|
|
@ -1,8 +1,4 @@
|
||||||
import os
|
|
||||||
import secrets
|
|
||||||
import sys
|
|
||||||
from base64 import b64decode, b64encode
|
from base64 import b64decode, b64encode
|
||||||
|
|
||||||
from colorama import Back
|
from colorama import Back
|
||||||
from colorama import Fore
|
from colorama import Fore
|
||||||
from colorama import init as colorama_init
|
from colorama import init as colorama_init
|
||||||
|
@ -17,6 +13,9 @@ from cryptography.hazmat.primitives.ciphers import modes
|
||||||
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
|
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
|
||||||
from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
|
from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
|
||||||
from flask import current_app
|
from flask import current_app
|
||||||
|
import os
|
||||||
|
import secrets
|
||||||
|
import sys
|
||||||
|
|
||||||
global_iv_length = 16
|
global_iv_length = 16
|
||||||
global_key_shares = 5 # Number of generated key shares
|
global_key_shares = 5 # Number of generated key shares
|
||||||
|
@ -26,6 +25,7 @@ backend_root_key_name = "root_key"
|
||||||
backend_encrypt_key_name = "encrypt_key"
|
backend_encrypt_key_name = "encrypt_key"
|
||||||
backend_root_key_salt_name = "root_key_salt"
|
backend_root_key_salt_name = "root_key_salt"
|
||||||
backend_encrypt_key_salt_name = "encrypt_key_salt"
|
backend_encrypt_key_salt_name = "encrypt_key_salt"
|
||||||
|
backend_seal_key = "seal_status"
|
||||||
success = "success"
|
success = "success"
|
||||||
seal_status = True
|
seal_status = True
|
||||||
|
|
||||||
|
@ -51,6 +51,9 @@ class Backend:
|
||||||
def add(self, key, value):
|
def add(self, key, value):
|
||||||
return self.backend.add(key, value)
|
return self.backend.add(key, value)
|
||||||
|
|
||||||
|
def update(self, key, value):
|
||||||
|
return self.backend.update(key, value)
|
||||||
|
|
||||||
|
|
||||||
class KeyManage:
|
class KeyManage:
|
||||||
|
|
||||||
|
@ -61,13 +64,13 @@ class KeyManage:
|
||||||
self.backend = Backend(backend)
|
self.backend = Backend(backend)
|
||||||
|
|
||||||
def init_app(self, app, backend=None):
|
def init_app(self, app, backend=None):
|
||||||
self.trigger = app.config.get("INNER_TRIGGER_TOKEN")
|
if sys.argv[0].endswith("gunicorn") or sys.argv[1] == "run":
|
||||||
if not self.trigger:
|
self.trigger = app.config.get("INNER_TRIGGER_TOKEN")
|
||||||
return
|
if not self.trigger:
|
||||||
self.backend = backend
|
return
|
||||||
|
self.backend = backend
|
||||||
resp = self.auto_unseal()
|
resp = self.auto_unseal()
|
||||||
self.print_response(resp)
|
self.print_response(resp)
|
||||||
|
|
||||||
def hash_root_key(self, value):
|
def hash_root_key(self, value):
|
||||||
algorithm = hashes.SHA256()
|
algorithm = hashes.SHA256()
|
||||||
|
@ -118,23 +121,23 @@ class KeyManage:
|
||||||
|
|
||||||
return new_shares
|
return new_shares
|
||||||
|
|
||||||
def auth_root_secret(self, root_key):
|
def is_valid_root_key(self, root_key):
|
||||||
root_key_hash, ok = self.hash_root_key(root_key)
|
root_key_hash, ok = self.hash_root_key(root_key)
|
||||||
if not ok:
|
if not ok:
|
||||||
return {
|
return root_key_hash, ok
|
||||||
"message": root_key_hash,
|
|
||||||
"status": "failed"
|
|
||||||
}
|
|
||||||
|
|
||||||
backend_root_key_hash = self.backend.get(backend_root_key_name)
|
backend_root_key_hash = self.backend.get(backend_root_key_name)
|
||||||
if not backend_root_key_hash:
|
if not backend_root_key_hash:
|
||||||
return {
|
return "should init firstly", False
|
||||||
"message": "should init firstly",
|
|
||||||
"status": "failed"
|
|
||||||
}
|
|
||||||
elif backend_root_key_hash != root_key_hash:
|
elif backend_root_key_hash != root_key_hash:
|
||||||
|
return "invalid root key", False
|
||||||
|
else:
|
||||||
|
return "", True
|
||||||
|
|
||||||
|
def auth_root_secret(self, root_key):
|
||||||
|
msg, ok = self.is_valid_root_key(root_key)
|
||||||
|
if not ok:
|
||||||
return {
|
return {
|
||||||
"message": "invalid root key",
|
"message": msg,
|
||||||
"status": "failed"
|
"status": "failed"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -147,10 +150,13 @@ class KeyManage:
|
||||||
|
|
||||||
secrets_encrypt_key, ok = InnerCrypt.aes_decrypt(string_to_bytes(root_key), encrypt_key_aes)
|
secrets_encrypt_key, ok = InnerCrypt.aes_decrypt(string_to_bytes(root_key), encrypt_key_aes)
|
||||||
if ok:
|
if ok:
|
||||||
current_app.config["secrets_encrypt_key"] = secrets_encrypt_key
|
msg, ok = self.backend.update(backend_seal_key, "open")
|
||||||
current_app.config["secrets_root_key"] = root_key
|
if ok:
|
||||||
current_app.config["secrets_shares"] = []
|
current_app.config["secrets_encrypt_key"] = secrets_encrypt_key
|
||||||
return {"message": success, "status": success}
|
current_app.config["secrets_root_key"] = root_key
|
||||||
|
current_app.config["secrets_shares"] = []
|
||||||
|
return {"message": success, "status": success}
|
||||||
|
return {"message": msg, "status": "failed"}
|
||||||
else:
|
else:
|
||||||
return {
|
return {
|
||||||
"message": secrets_encrypt_key,
|
"message": secrets_encrypt_key,
|
||||||
|
@ -204,34 +210,36 @@ class KeyManage:
|
||||||
"""
|
"""
|
||||||
root_key = self.backend.get(backend_root_key_name)
|
root_key = self.backend.get(backend_root_key_name)
|
||||||
if root_key:
|
if root_key:
|
||||||
return {"message": "already init, skip"}, False
|
return {"message": "already init, skip", "status": "skip"}, False
|
||||||
else:
|
else:
|
||||||
root_key, shares, status = self.generate_unseal_keys()
|
root_key, shares, status = self.generate_unseal_keys()
|
||||||
if not status:
|
if not status:
|
||||||
return {"message": root_key}, False
|
return {"message": root_key, "status": "failed"}, False
|
||||||
|
|
||||||
# hash root key and store in backend
|
# hash root key and store in backend
|
||||||
root_key_hash, ok = self.hash_root_key(root_key)
|
root_key_hash, ok = self.hash_root_key(root_key)
|
||||||
if not ok:
|
if not ok:
|
||||||
return {"message": root_key_hash}, False
|
return {"message": root_key_hash, "status": "failed"}, False
|
||||||
|
|
||||||
msg, ok = self.backend.add(backend_root_key_name, root_key_hash)
|
msg, ok = self.backend.add(backend_root_key_name, root_key_hash)
|
||||||
if not ok:
|
if not ok:
|
||||||
return {"message": msg}, False
|
return {"message": msg, "status": "failed"}, False
|
||||||
|
|
||||||
# generate encrypt key from root_key and store in backend
|
# generate encrypt key from root_key and store in backend
|
||||||
encrypt_key, ok = self.generate_encrypt_key(root_key)
|
encrypt_key, ok = self.generate_encrypt_key(root_key)
|
||||||
if not ok:
|
if not ok:
|
||||||
return {"message": encrypt_key}
|
return {"message": encrypt_key, "status": "failed"}
|
||||||
|
|
||||||
encrypt_key_aes, status = InnerCrypt.aes_encrypt(root_key, encrypt_key)
|
encrypt_key_aes, status = InnerCrypt.aes_encrypt(root_key, encrypt_key)
|
||||||
if not status:
|
if not status:
|
||||||
return {"message": encrypt_key_aes}
|
return {"message": encrypt_key_aes, "status": "failed"}
|
||||||
|
|
||||||
msg, ok = self.backend.add(backend_encrypt_key_name, encrypt_key_aes)
|
msg, ok = self.backend.add(backend_encrypt_key_name, encrypt_key_aes)
|
||||||
if not ok:
|
if not ok:
|
||||||
return {"message": msg}, False
|
return {"message": msg, "status": "failed"}, False
|
||||||
|
msg, ok = self.backend.add(backend_seal_key, "open")
|
||||||
|
if not ok:
|
||||||
|
return {"message": msg, "status": "failed"}, False
|
||||||
current_app.config["secrets_root_key"] = root_key
|
current_app.config["secrets_root_key"] = root_key
|
||||||
current_app.config["secrets_encrypt_key"] = encrypt_key
|
current_app.config["secrets_encrypt_key"] = encrypt_key
|
||||||
self.print_token(shares, root_token=root_key)
|
self.print_token(shares, root_token=root_key)
|
||||||
|
@ -275,28 +283,21 @@ class KeyManage:
|
||||||
|
|
||||||
def seal(self, root_key):
|
def seal(self, root_key):
|
||||||
root_key = root_key.encode()
|
root_key = root_key.encode()
|
||||||
root_key_hash, ok = self.hash_root_key(root_key)
|
msg, ok = self.is_valid_root_key(root_key)
|
||||||
if not ok:
|
if not ok:
|
||||||
return {
|
return {
|
||||||
"message": root_key_hash,
|
"message": msg,
|
||||||
"status": "failed"
|
|
||||||
}
|
|
||||||
|
|
||||||
backend_root_key_hash = self.backend.get(backend_root_key_name)
|
|
||||||
if not backend_root_key_hash:
|
|
||||||
return {
|
|
||||||
"message": "not init, seal skip",
|
|
||||||
"status": "skip"
|
|
||||||
}
|
|
||||||
elif root_key_hash != backend_root_key_hash:
|
|
||||||
return {
|
|
||||||
"message": "invalid root key",
|
|
||||||
"status": "failed"
|
"status": "failed"
|
||||||
}
|
}
|
||||||
else:
|
else:
|
||||||
|
msg, ok = self.backend.update(backend_seal_key, "block")
|
||||||
|
if not ok:
|
||||||
|
return {
|
||||||
|
"message": msg,
|
||||||
|
"status": "failed",
|
||||||
|
}
|
||||||
current_app.config["secrets_root_key"] = ''
|
current_app.config["secrets_root_key"] = ''
|
||||||
current_app.config["secrets_encrypt_key"] = ''
|
current_app.config["secrets_encrypt_key"] = ''
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"message": success,
|
"message": success,
|
||||||
"status": success
|
"status": success
|
||||||
|
@ -308,11 +309,11 @@ class KeyManage:
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
secrets_root_key = current_app.config.get("secrets_root_key")
|
secrets_root_key = current_app.config.get("secrets_root_key")
|
||||||
root_key = self.backend.get(backend_root_key_name)
|
msg, ok = self.is_valid_root_key(secrets_root_key)
|
||||||
if root_key == "" or root_key != secrets_root_key:
|
if not ok:
|
||||||
return "invalid root key", True
|
return {"message": msg, "status": "failed"}
|
||||||
|
status = self.backend.get(backend_seal_key)
|
||||||
return "", False
|
return status == "block"
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def print_token(cls, shares, root_token):
|
def print_token(cls, shares, root_token):
|
||||||
|
@ -330,7 +331,7 @@ class KeyManage:
|
||||||
|
|
||||||
for i, v in enumerate(shares):
|
for i, v in enumerate(shares):
|
||||||
print(
|
print(
|
||||||
"unseal token " + str(i + 1) + ": " + Fore.RED + Back.CYAN + v.decode("utf-8") + Style.RESET_ALL)
|
"unseal token " + str(i + 1) + ": " + Fore.RED + Back.BLACK + v.decode("utf-8") + Style.RESET_ALL)
|
||||||
print()
|
print()
|
||||||
|
|
||||||
print(Fore.GREEN + "root token: " + root_token.decode("utf-8") + Style.RESET_ALL)
|
print(Fore.GREEN + "root token: " + root_token.decode("utf-8") + Style.RESET_ALL)
|
||||||
|
@ -339,14 +340,12 @@ class KeyManage:
|
||||||
def print_response(cls, data):
|
def print_response(cls, data):
|
||||||
status = data.get("status", "")
|
status = data.get("status", "")
|
||||||
message = data.get("message", "")
|
message = data.get("message", "")
|
||||||
if status == "skip":
|
status_colors = {
|
||||||
print(Style.BRIGHT, message)
|
"skip": Style.BRIGHT,
|
||||||
elif status == "failed":
|
"failed": Fore.RED,
|
||||||
print(Fore.RED, message)
|
"waiting": Fore.YELLOW,
|
||||||
elif status == "waiting":
|
}
|
||||||
print(Fore.YELLOW, message)
|
print(status_colors.get(status, Fore.GREEN), message, Style.RESET_ALL)
|
||||||
else:
|
|
||||||
print(Fore.GREEN, message)
|
|
||||||
|
|
||||||
|
|
||||||
class InnerCrypt:
|
class InnerCrypt:
|
||||||
|
|
|
@ -11,7 +11,6 @@ class InnerKVManger(object):
|
||||||
res = InnerKV.create(**data)
|
res = InnerKV.create(**data)
|
||||||
if res.key == key:
|
if res.key == key:
|
||||||
return "success", True
|
return "success", True
|
||||||
|
|
||||||
return "add failed", False
|
return "add failed", False
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
|
@ -21,3 +20,14 @@ class InnerKVManger(object):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
return res.value
|
return res.value
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def update(cls, key, value):
|
||||||
|
res = InnerKV.get_by(first=True, to_dict=False, **{"key": key})
|
||||||
|
if not res:
|
||||||
|
return None
|
||||||
|
res.value = value
|
||||||
|
t = res.update()
|
||||||
|
if t.key == key:
|
||||||
|
return "success", True
|
||||||
|
return "update failed", True
|
||||||
|
|
|
@ -1,38 +1,38 @@
|
||||||
|
from api.lib.perm.auth import auth_abandoned
|
||||||
from api.resource import APIView
|
from api.resource import APIView
|
||||||
from api.lib.secrets.inner import KeyManage
|
from api.lib.secrets.inner import KeyManage
|
||||||
from api.lib.secrets.secrets import InnerKVManger
|
from api.lib.secrets.secrets import InnerKVManger
|
||||||
|
|
||||||
from flask import request, abort
|
from flask import current_app
|
||||||
|
from flask import request
|
||||||
|
|
||||||
|
|
||||||
class InnerSecretUnSealView(APIView):
|
class InnerSecretUnSealView(APIView):
|
||||||
url_prefix = "/secrets/unseal"
|
url_prefix = "/secrets/unseal"
|
||||||
|
|
||||||
|
@auth_abandoned
|
||||||
def post(self):
|
def post(self):
|
||||||
unseal_key = request.headers.get("Unseal-Token")
|
unseal_key = request.headers.get("Unseal-Token")
|
||||||
res = KeyManage(backend=InnerKVManger()).unseal(unseal_key)
|
res = KeyManage(backend=InnerKVManger()).unseal(unseal_key)
|
||||||
# if res.get("status") == "failed":
|
|
||||||
# return abort(400, res.get("message"))
|
|
||||||
return self.jsonify(**res)
|
return self.jsonify(**res)
|
||||||
|
|
||||||
|
|
||||||
class InnerSecretSealView(APIView):
|
class InnerSecretSealView(APIView):
|
||||||
url_prefix = "/secrets/seal"
|
url_prefix = "/secrets/seal"
|
||||||
|
|
||||||
|
@auth_abandoned
|
||||||
def post(self):
|
def post(self):
|
||||||
unseal_key = request.headers.get("Inner-Token")
|
unseal_key = request.headers.get("Inner-Token")
|
||||||
res = KeyManage(backend=InnerKVManger()).seal(unseal_key)
|
res = KeyManage(backend=InnerKVManger()).seal(unseal_key)
|
||||||
# if res.get("status") == "failed":
|
|
||||||
# return abort(400, res.get("message"))
|
|
||||||
return self.jsonify(**res)
|
return self.jsonify(**res)
|
||||||
|
|
||||||
|
|
||||||
class InnerSecretAutoSealView(APIView):
|
class InnerSecretAutoSealView(APIView):
|
||||||
url_prefix = "/secrets/auto_seal"
|
url_prefix = "/secrets/auto_seal"
|
||||||
|
|
||||||
|
@auth_abandoned
|
||||||
def post(self):
|
def post(self):
|
||||||
unseal_key = request.headers.get("Inner-Token")
|
root_key = request.headers.get("Inner-Token")
|
||||||
res = KeyManage(backend=InnerKVManger()).seal(unseal_key)
|
res = KeyManage(trigger=root_key,
|
||||||
# if res.get("status") == "failed":
|
backend=InnerKVManger()).auto_unseal()
|
||||||
# return abort(400, res.get("message"))
|
|
||||||
return self.jsonify(**res)
|
return self.jsonify(**res)
|
||||||
|
|
Loading…
Reference in New Issue