|
14 | 14 | import salt.crypt
|
15 | 15 | import salt.exceptions
|
16 | 16 | import salt.payload
|
17 |
| -import salt.transport |
| 17 | +import salt.sqlalchemy |
18 | 18 | import salt.utils.args
|
19 | 19 | import salt.utils.crypt
|
20 | 20 | import salt.utils.data
|
21 | 21 | import salt.utils.event
|
22 | 22 | import salt.utils.files
|
23 |
| -import salt.utils.json |
24 | 23 | import salt.utils.kinds
|
25 |
| -import salt.utils.minions |
26 | 24 | import salt.utils.sdb
|
27 |
| -import salt.utils.stringutils |
28 |
| -import salt.utils.user |
29 | 25 | from salt.utils.decorators import cached_property
|
30 | 26 |
|
31 | 27 | log = logging.getLogger(__name__)
|
32 | 28 |
|
33 | 29 |
|
34 | 30 | def get_key(opts):
|
35 |
| - return Key(opts) |
| 31 | + if opts.get("keys.cache_driver") == "sqlalchemy": |
| 32 | + if not salt.sqlalchemy.orm_configured(): |
| 33 | + salt.sqlalchemy.configure_orm(opts) |
| 34 | + |
| 35 | + return SqlAlchemyKey(opts) |
| 36 | + else: |
| 37 | + return Key(opts) |
36 | 38 |
|
37 | 39 |
|
38 | 40 | class KeyCLI:
|
@@ -562,14 +564,12 @@ def dict_match(self, match_dict):
|
562 | 564 | Accept a dictionary of keys and return the current state of the
|
563 | 565 | specified keys
|
564 | 566 | """
|
565 |
| - ret = {} |
566 |
| - cur_keys = self.list_keys() |
567 |
| - for status, keys in match_dict.items(): |
568 |
| - for key in salt.utils.data.sorted_ignorecase(keys): |
569 |
| - for keydir in (self.ACC, self.PEND, self.REJ, self.DEN): |
570 |
| - if keydir and fnmatch.filter(cur_keys.get(keydir, []), key): |
571 |
| - ret.setdefault(keydir, []).append(key) |
572 |
| - return ret |
| 567 | + matches = [] |
| 568 | + # not sure why this interface was ever added. just toss to glob match |
| 569 | + for match_list in match_dict.values(): |
| 570 | + matches.extend(match_list) |
| 571 | + |
| 572 | + return self.glob_match(matches) |
573 | 573 |
|
574 | 574 | def list_keys(self):
|
575 | 575 | """
|
@@ -797,14 +797,21 @@ def delete_key(
|
797 | 797 | "master AES key is rotated or auth is revoked "
|
798 | 798 | "with 'saltutil.revoke_auth'.".format(key)
|
799 | 799 | )
|
| 800 | + deleted = False |
800 | 801 | if status == "minions_denied":
|
801 | 802 | self.cache.flush("denied_keys", key)
|
| 803 | + deleted = True |
802 | 804 | else:
|
803 |
| - self.cache.flush("keys", key) |
804 |
| - eload = {"result": True, "act": "delete", "id": key} |
805 |
| - self.event.fire_event( |
806 |
| - eload, salt.utils.event.tagify(prefix="key") |
807 |
| - ) |
| 805 | + val = self.cache.fetch("keys", key) |
| 806 | + if val and self.STATE_MAP[val["state"]] == status: |
| 807 | + self.cache.flush("keys", key) |
| 808 | + deleted = True |
| 809 | + |
| 810 | + if deleted: |
| 811 | + eload = {"result": True, "act": "delete", "id": key} |
| 812 | + self.event.fire_event( |
| 813 | + eload, salt.utils.event.tagify(prefix="key") |
| 814 | + ) |
808 | 815 | except OSError:
|
809 | 816 | pass
|
810 | 817 | if self.opts.get("preserve_minions") is True:
|
@@ -925,3 +932,123 @@ def __enter__(self):
|
925 | 932 |
|
926 | 933 | def __exit__(self, *args):
|
927 | 934 | self.event.destroy()
|
| 935 | + |
| 936 | + |
| 937 | +try: |
| 938 | + from sqlalchemy import or_, select |
| 939 | + |
| 940 | + from salt.sqlalchemy.models import model_for |
| 941 | +except ImportError: |
| 942 | + # stubs to appease pyright |
| 943 | + model_for = select = or_ = print |
| 944 | + |
| 945 | + |
| 946 | +class SqlAlchemyKey(Key): |
| 947 | + def __init__(self, opts, *args, **kwargs): |
| 948 | + # unfortunately real null can't be used as a pk in certain backends so |
| 949 | + # it must be mapped to a string null |
| 950 | + self.cluster_id = opts["cluster_id"] or "null" |
| 951 | + super().__init__(opts, *args, **kwargs) |
| 952 | + |
| 953 | + def list_keys(self): |
| 954 | + """ |
| 955 | + Return a dict of managed keys and what the key status are |
| 956 | + """ |
| 957 | + ret = { |
| 958 | + "minions_pre": [], |
| 959 | + "minions_rejected": [], |
| 960 | + "minions": [], |
| 961 | + "minions_denied": [], |
| 962 | + } |
| 963 | + |
| 964 | + Cache = model_for("Cache") |
| 965 | + with salt.sqlalchemy.ROSession() as session: |
| 966 | + stmt = select(Cache.key, Cache.data["state"]).where( |
| 967 | + or_(Cache.bank == "keys", Cache.bank == "denied_keys"), |
| 968 | + Cache.cluster == self.cluster_id, |
| 969 | + ) |
| 970 | + results = session.execute(stmt).all() |
| 971 | + session.commit() |
| 972 | + |
| 973 | + for id_, state in results: |
| 974 | + if state == "accepted": |
| 975 | + ret["minions"].append(id_) |
| 976 | + elif state == "pending": |
| 977 | + ret["minions_pre"].append(id_) |
| 978 | + elif state == "rejected": |
| 979 | + ret["minions_rejected"].append(id_) |
| 980 | + |
| 981 | + for id_ in self.cache.list("denied_keys"): |
| 982 | + ret["minions_denied"].append(id_) |
| 983 | + |
| 984 | + return ret |
| 985 | + |
| 986 | + def list_match(self, match): |
| 987 | + """ |
| 988 | + Accept a glob which to match the of a key and return the key's location |
| 989 | + """ |
| 990 | + ret = {} |
| 991 | + if isinstance(match, str): |
| 992 | + match = match.split(",") |
| 993 | + |
| 994 | + ret = {} |
| 995 | + |
| 996 | + Cache = model_for("Cache") |
| 997 | + with salt.sqlalchemy.ROSession() as session: |
| 998 | + stmt = select(Cache.key, Cache.bank, Cache.data["state"]).where( |
| 999 | + Cache.cluster == self.cluster_id, |
| 1000 | + Cache.key.in_(match), |
| 1001 | + or_(Cache.bank == "keys", Cache.bank == "denied_keys"), |
| 1002 | + ) |
| 1003 | + |
| 1004 | + results = session.execute(stmt).all() |
| 1005 | + for _id, bank, state in results: |
| 1006 | + # backward compatibility stuff requires denied_keys be handled differently |
| 1007 | + if bank == "denied_keys": |
| 1008 | + state = "denied" |
| 1009 | + |
| 1010 | + ret.setdefault(self.STATE_MAP[state], []) |
| 1011 | + ret[self.STATE_MAP[state]].append(_id) |
| 1012 | + session.commit() |
| 1013 | + |
| 1014 | + return ret |
| 1015 | + |
| 1016 | + def glob_match(self, match, full=False): # pylint: disable=unused-argument |
| 1017 | + """ |
| 1018 | + Return the minions found by looking via globs |
| 1019 | + """ |
| 1020 | + ret = {} |
| 1021 | + |
| 1022 | + # optimization: |
| 1023 | + # if there is no glob in the expression, we can just treat it as a single element list |
| 1024 | + if isinstance(match, str) and "*" not in match: |
| 1025 | + return self.list_match(match) |
| 1026 | + |
| 1027 | + if isinstance(match, list): |
| 1028 | + match = ",".join(match) |
| 1029 | + |
| 1030 | + # we want to translate shell globs to an equivalent ILIKE query |
| 1031 | + match = match.replace("_", "\\_") |
| 1032 | + match = match.replace("%", "\\%") |
| 1033 | + match = match.replace("*", "%") |
| 1034 | + match = match.split(",") |
| 1035 | + |
| 1036 | + Cache = model_for("Cache") |
| 1037 | + with salt.sqlalchemy.ROSession() as session: |
| 1038 | + stmt = select(Cache.key, Cache.bank, Cache.data["state"]).where( |
| 1039 | + Cache.cluster == self.cluster_id, |
| 1040 | + or_(Cache.bank == "keys", Cache.bank == "denied_keys"), |
| 1041 | + or_(Cache.key.ilike(ex) for ex in match), |
| 1042 | + ) |
| 1043 | + |
| 1044 | + results = session.execute(stmt).all() |
| 1045 | + for _id, bank, state in results: |
| 1046 | + # backward compatibility stuff requires denied_keys be handled differently |
| 1047 | + if bank == "denied_keys": |
| 1048 | + state = "denied" |
| 1049 | + |
| 1050 | + ret.setdefault(self.STATE_MAP[state], []) |
| 1051 | + ret[self.STATE_MAP[state]].append(_id) |
| 1052 | + session.commit() |
| 1053 | + |
| 1054 | + return ret |
0 commit comments