馃悶 Fixed bug #32 馃悶

parent b4a6ca6b
......@@ -33,7 +33,7 @@ CONFIG_HTTP = {
}
CONFIG_NACM = {
"ALLOWED_USERS": ["lojza@mail.tld"]
"ALLOWED_USERS": []
}
CONFIG_KNOT = {
......@@ -47,14 +47,12 @@ CONFIG = {
"KNOT": CONFIG_KNOT
}
NACM_ADMINS = CONFIG["NACM"]["ALLOWED_USERS"]
API_ROOT_data = os.path.join(CONFIG_HTTP["API_ROOT"], "data")
API_ROOT_STAGING_data = os.path.join(CONFIG_HTTP["API_ROOT_STAGING"], "data")
API_ROOT_ops = os.path.join(CONFIG_HTTP["API_ROOT"], "operations")
def load_config(filename: str) -> bool:
global NACM_ADMINS
global API_ROOT_data
global API_ROOT_STAGING_data
global API_ROOT_ops
......@@ -68,7 +66,6 @@ def load_config(filename: str) -> bool:
pass
# Shortcuts
NACM_ADMINS = CONFIG["NACM"]["ALLOWED_USERS"]
API_ROOT_data = os.path.join(CONFIG_HTTP["API_ROOT"], "data")
API_ROOT_STAGING_data = os.path.join(CONFIG_HTTP["API_ROOT_STAGING"], "data")
API_ROOT_ops = os.path.join(CONFIG_HTTP["API_ROOT"], "operations")
......
......@@ -14,26 +14,28 @@ from yangson.schemanode import (
SchemaError,
SemanticError,
InternalNode,
ContainerNode)
ContainerNode
)
from yangson.instvalue import ArrayValue, ObjectValue
from yangson.instance import (
InstanceNode,
NonexistentInstance,
InstanceValueError,
ArrayValue,
ObjectValue,
MemberName,
EntryKeys,
EntryIndex,
InstanceRoute,
ArrayEntry,
RootNode,
ObjectMember)
ObjectMember
)
from .helpers import PathFormat, ErrorHelpers, LogHelpers, DataHelpers, JsonNodeT
from .config import CONFIG
from .nacm import NacmConfig, Permission, Action
from .config import CONFIG, CONFIG_NACM, CONFIG_HTTP
from .nacm import NacmConfig, Permission, Action, NacmForbiddenError
from .handler_list import OP_HANDLERS, STATE_DATA_HANDLES, CONF_DATA_HANDLES, ConfDataObjectHandler, ConfDataListHandler
from .usr_state_data_handlers import ContainerNodeHandlerBase, ListNodeHandlerBase
from .errors import JetconfError
epretty = ErrorHelpers.epretty
debug_data = LogHelpers.create_module_dbg_logger(__name__)
......@@ -45,37 +47,20 @@ class ChangeType(Enum):
DELETE = 2
class NacmForbiddenError(Exception):
def __init__(self, msg="Access to data node rejected by NACM", rule=None):
self.msg = msg
self.rulename = rule
def __str__(self):
return self.msg
class DataLockError(Exception):
def __init__(self, msg=""):
self.msg = msg
def __str__(self):
return self.msg
class DataLockError(JetconfError):
pass
class InstanceAlreadyPresent(Exception):
def __init__(self, msg=""):
self.msg = msg
class NoStagingDataException(JetconfError):
pass
def __str__(self):
return self.msg
class InstanceAlreadyPresent(JetconfError):
pass
class HandlerError(Exception):
def __init__(self, msg=""):
self.msg = msg
def __str__(self):
return self.msg
class HandlerError(JetconfError):
pass
class NoHandlerError(HandlerError):
......@@ -277,7 +262,7 @@ class BaseDatastore:
root = usr_journal.get_root_head()
return root
else:
raise NoHandlerError("No active changelist for user \"{}\"".format(username))
raise NoStagingDataException("No active changelist for user \"{}\"".format(username))
# Set a new Instance node as data root, store old root to archive
def set_data_root(self, new_root: InstanceNode):
......@@ -348,8 +333,8 @@ class BaseDatastore:
h = CONF_DATA_HANDLES.get_handler(str(id(sn)))
if h is not None and isinstance(h, ConfDataObjectHandler):
info("handler for superior data node triggered, replace")
print(h.schema_path)
print(h.__class__.__name__)
# print(h.schema_path)
# print(h.__class__.__name__)
h.replace(ii, ch)
if h is not None and isinstance(h, ConfDataListHandler):
info("handler for superior data node triggered, replace_item")
......@@ -359,22 +344,37 @@ class BaseDatastore:
warn("Cannnot notify {}, parent container removed".format(ii))
# Get data node, evaluate NACM if required
def get_node_rpc(self, rpc: RpcInfo, yl_data=False, staging=False) -> InstanceNode:
if rpc.path == "":
ii = []
else:
ii = self.parse_ii(rpc.path, rpc.path_format)
def get_node_rpc(self, rpc: RpcInfo, staging=False) -> InstanceNode:
ii = self.parse_ii(rpc.path, rpc.path_format)
if yl_data:
root = self._yang_lib_data
elif staging:
if staging:
try:
root = self.get_data_root_staging(rpc.username)
except NoHandlerError:
except NoStagingDataException:
root = self._data
else:
root = self._data
if (len(ii) > 0) and (isinstance(ii[0], MemberName)):
# Not getting root
ns_first = ii[0].namespace
if (ns_first == "ietf-netconf-acm") and (rpc.username not in CONFIG_NACM["ALLOWED_USERS"]):
raise NacmForbiddenError(rpc.username + " not allowed to access NACM data")
elif ns_first == "ietf-yang-library":
root = self._yang_lib_data
else:
# Root node requested
# Remove NACM data if user is not NACM privieged
if rpc.username not in CONFIG_NACM["ALLOWED_USERS"]:
try:
root = root.delete_item("ietf-netconf-acm:nacm")
except NonexistentInstance:
pass
# Append YANG library data
for member_name, member_val in self._yang_lib_data.value.items():
root = root.put_member(member_name, member_val)
# Resolve schema node of the desired data node
sch_pth_list = filter(lambda isel: isinstance(isel, MemberName), ii)
sch_pth = DataHelpers.ii2str(sch_pth_list)
......@@ -382,13 +382,14 @@ class BaseDatastore:
state_roots = sn.state_roots()
if state_roots and not yl_data:
# Check if URL points to state data or node that contains state data
if state_roots:
debug_data("State roots: {}".format(state_roots))
for state_root_sch_pth in state_roots:
state_root_sn = self._dm.get_data_node(state_root_sch_pth)
# Check if desired node is child of state root
# Check if the desired node is child of the state root
sni = sn
is_child = False
while sni:
......@@ -431,7 +432,6 @@ class BaseDatastore:
if node.schema_node is state_root_sn.parent:
ii_gen = DataHelpers.node_get_ii(node)
sdh = STATE_DATA_HANDLES.get_handler(state_root_sch_pth)
# print(state_root_sch_pth)
if sdh is not None:
try:
if isinstance(sdh, ContainerNodeHandlerBase):
......@@ -466,8 +466,10 @@ class BaseDatastore:
n = _fill_state_roots(n)
root = n.top()
else:
# No state data in requested node
n = root.goto(ii)
# Process "with-defaults" query parameter
try:
with_defs = rpc.qs["with-defaults"][0]
except (IndexError, KeyError):
......@@ -476,14 +478,16 @@ class BaseDatastore:
if with_defs == "report-all":
n = n.add_defaults()
# Evaluate NACM if required
if self.nacm:
nrpc = self.nacm.get_user_nacm(rpc.username)
nrpc = self.nacm.get_user_rules(rpc.username)
if nrpc.check_data_node_permission(root, ii, Permission.NACM_ACCESS_READ) == Action.DENY:
raise NacmForbiddenError()
else:
# Prune nodes that should not be accessible to user
n = nrpc.prune_data_tree(n, root, ii, Permission.NACM_ACCESS_READ)
# Process "depth" query parameter
try:
max_depth_str = rpc.qs["depth"][0]
if max_depth_str == "unbounded":
......@@ -517,6 +521,7 @@ class BaseDatastore:
return node
n = _tree_limit_depth(n, 1)
# Return result
return n
# Create new data node (Restconf draft compliant version)
......@@ -528,7 +533,7 @@ class BaseDatastore:
point = rpc.qs.get("point", [None])[0]
if self.nacm:
nrpc = self.nacm.get_user_nacm(rpc.username)
nrpc = self.nacm.get_user_rules(rpc.username)
if nrpc.check_data_node_permission(root, ii, Permission.NACM_ACCESS_CREATE) == Action.DENY:
raise NacmForbiddenError()
......@@ -633,7 +638,7 @@ class BaseDatastore:
n = root.goto(ii)
if self.nacm:
nrpc = self.nacm.get_user_nacm(rpc.username)
nrpc = self.nacm.get_user_rules(rpc.username)
if nrpc.check_data_node_permission(root, ii, Permission.NACM_ACCESS_UPDATE) == Action.DENY:
raise NacmForbiddenError()
......@@ -657,7 +662,7 @@ class BaseDatastore:
last_isel = ii[-1]
if self.nacm:
nrpc = self.nacm.get_user_nacm(rpc.username)
nrpc = self.nacm.get_user_rules(rpc.username)
if nrpc.check_data_node_permission(root, ii, Permission.NACM_ACCESS_DELETE) == Action.DENY:
raise NacmForbiddenError()
......@@ -750,7 +755,7 @@ class BaseDatastore:
else:
# User-defined operation
if self.nacm and (not rpc.skip_nacm_check):
nrpc = self.nacm.get_user_nacm(rpc.username)
nrpc = self.nacm.get_user_rules(rpc.username)
if nrpc.check_rpc_name(rpc.op_name) == Action.DENY:
raise NacmForbiddenError(
"Op \"{}\" invocation denied for user \"{}\"".format(rpc.op_name, rpc.username)
......
# Base class for all exceptions defined in jetconf
class JetconfError(Exception):
def __init__(self, msg=""):
self.msg = msg
def __str__(self):
return self.msg
......@@ -48,10 +48,14 @@ class DataHelpers:
# Get the namespace of the first segment in path
# Raises ValueError if the first segment is not in fully-qualified format
# Returns empty string if api_pth is empty
@staticmethod
def path_first_ns(api_pth: str) -> str:
first_seg = api_pth[1:].split("/", maxsplit=1)[0]
ns1, sel1 = first_seg.split(":", maxsplit=1)
if (len(api_pth) > 0) and (api_pth[0] == "/"):
first_seg = api_pth[1:].split("/", maxsplit=1)[0]
ns1, sel1 = first_seg.split(":", maxsplit=1)
else:
ns1 = ""
return ns1
@staticmethod
......
......@@ -13,19 +13,20 @@ from yangson.schemanode import ContainerNode, ListNode, GroupNode, LeafListNode,
from yangson.instance import NonexistentInstance, InstanceValueError, RootNode
from yangson.datatype import YangTypeError
from jetconf.knot_api import KnotError
from .config import CONFIG_GLOBAL, CONFIG_HTTP, NACM_ADMINS, API_ROOT_data, API_ROOT_STAGING_data, API_ROOT_ops
from .knot_api import KnotError
from .config import CONFIG_GLOBAL, CONFIG_HTTP, CONFIG_NACM, API_ROOT_data, API_ROOT_STAGING_data, API_ROOT_ops
from .helpers import CertHelpers, DataHelpers, DateTimeHelpers, ErrorHelpers, LogHelpers, SSLCertT
from .nacm import NacmForbiddenError
from .data import (
BaseDatastore,
RpcInfo,
DataLockError,
NacmForbiddenError,
NoHandlerError,
NoHandlerForOpError,
InstanceAlreadyPresent,
ChangeType,
ConfHandlerFailedError)
ConfHandlerFailedError
)
QueryStrT = Dict[str, List[str]]
epretty = ErrorHelpers.epretty
......@@ -163,7 +164,7 @@ def api_root_handler(headers: OrderedDict, data: Optional[str], client_cert: SSL
return HttpResponse(HttpStatus.Ok, response.encode(), CT_YANG_JSON)
def _get(ds: BaseDatastore, pth: str, username: str, yl_data: bool=False, staging: bool=False) -> HttpResponse:
def _get(ds: BaseDatastore, pth: str, username: str, staging: bool=False) -> HttpResponse:
url_split = pth.split("?")
url_path = url_split[0]
if len(url_split) > 1:
......@@ -182,7 +183,7 @@ def _get(ds: BaseDatastore, pth: str, username: str, yl_data: bool=False, stagin
n = None
http_resp = None
try:
n = ds.get_node_rpc(rpc1, yl_data, staging)
n = ds.get_node_rpc(rpc1, staging)
except NacmForbiddenError as e:
http_resp = HttpResponse.error(
HttpStatus.Forbidden,
......@@ -273,19 +274,7 @@ def create_get_api(ds: BaseDatastore):
info("[{}] api_get: {}".format(username, headers[":path"]))
api_pth = headers[":path"][len(API_ROOT_data):]
ns = DataHelpers.path_first_ns(api_pth)
if ns == "ietf-netconf-acm":
if username not in NACM_ADMINS:
warn(username + " not allowed to access NACM data")
http_resp = HttpResponse.empty(HttpStatus.Forbidden)
else:
http_resp = _get(ds.nacm.nacm_ds, api_pth, username)
elif ns == "ietf-yang-library":
http_resp = _get(ds, api_pth, username, yl_data=True)
else:
http_resp = _get(ds, api_pth, username)
http_resp = _get(ds, api_pth, username)
return http_resp
return get_api_closure
......@@ -300,7 +289,7 @@ def create_get_staging_api(ds: BaseDatastore):
ns = DataHelpers.path_first_ns(api_pth)
if ns == "ietf-netconf-acm":
if username not in NACM_ADMINS:
if username not in CONFIG_NACM["ALLOWED_USERS"]:
warn(username + " not allowed to access NACM data")
http_resp = HttpResponse.empty(HttpStatus.Forbidden)
else:
......@@ -395,7 +384,7 @@ def create_post_api(ds: BaseDatastore):
ns = DataHelpers.path_first_ns(api_pth)
if ns == "ietf-netconf-acm":
if username not in NACM_ADMINS:
if username not in CONFIG_NACM["ALLOWED_USERS"]:
warn(username + " not allowed to access NACM data")
http_resp = HttpResponse.empty(HttpStatus.Forbidden)
else:
......@@ -457,7 +446,7 @@ def create_put_api(ds: BaseDatastore):
ns = DataHelpers.path_first_ns(api_pth)
if ns == "ietf-netconf-acm":
if username not in NACM_ADMINS:
if username not in CONFIG_NACM["ALLOWED_USERS"]:
warn(username + " not allowed to access NACM data")
http_resp = HttpResponse.empty(HttpStatus.Forbidden)
else:
......@@ -511,7 +500,7 @@ def create_api_delete(ds: BaseDatastore):
ns = DataHelpers.path_first_ns(api_pth)
if ns == "ietf-netconf-acm":
if username not in NACM_ADMINS:
if username not in CONFIG_NACM["ALLOWED_USERS"]:
warn(username + " not allowed to access NACM data")
http_resp = HttpResponse.empty(HttpStatus.Forbidden)
else:
......@@ -562,7 +551,7 @@ def create_api_op(ds: BaseDatastore):
rpc1.op_input_args = input_args
# Skip NACM check for privileged users
if username in NACM_ADMINS:
if username in CONFIG_NACM["ALLOWED_USERS"]:
rpc1.skip_nacm_check = True
try:
......
......@@ -2,23 +2,25 @@ import collections
from io import StringIO
from threading import Lock
from enum import Enum
from typing import Union
from colorlog import error, info
from typing import List, Set, Optional
from yangson.datamodel import DataModel
from yangson.instvalue import Value, ArrayValue, ObjectValue
from yangson.instance import (
InstanceNode,
NonexistentSchemaNode,
NonexistentInstance,
ArrayValue,
ObjectValue,
InstanceRoute,
MemberName,
EntryIndex,
EntryKeys
)
from .helpers import DataHelpers, ErrorHelpers, LogHelpers, PathFormat
from .helpers import DataHelpers, ErrorHelpers, LogHelpers
from .errors import JetconfError
epretty = ErrorHelpers.epretty
debug_nacm = LogHelpers.create_module_dbg_logger(__name__)
......@@ -44,12 +46,21 @@ class NacmRuleType(Enum):
NACM_RULE_DATA = 3
class NonexistentUserError(Exception):
def __init__(self, msg=""):
class NacmError(JetconfError):
pass
class NonexistentUserError(NacmError):
pass
class NacmForbiddenError(NacmError):
def __init__(self, msg="Access to data node rejected by NACM", rule=None):
self.msg = msg
self.rule = rule
def __str__(self):
return self.msg
return "{} (rule: {})".format(self.msg, str(self.rule))
class NacmGroup:
......@@ -274,11 +285,11 @@ class NacmConfig:
return
info("Creating personalized rule list for user \"{}\"".format(username))
self._user_nacm_rpc[username] = UserNacm(self.dm, self, username)
self._user_nacm_rpc[username] = UserRuleSet(self.dm, self, username)
self.internal_data_lock.release()
def get_user_nacm(self, username: str) -> "UserNacm":
def get_user_rules(self, username: str) -> "UserRuleSet":
user_nacm = self._user_nacm_rpc.get(username)
if user_nacm is None:
self.create_user_nacm(username)
......@@ -288,7 +299,7 @@ class NacmConfig:
# Rules for particular user
class UserNacm:
class UserRuleSet:
def __init__(self, dm: DataModel, config: NacmConfig, username: str):
self.nacm_enabled = config.enabled
self.default_read = config.default_read
......@@ -307,7 +318,7 @@ class UserNacm:
if not self.nacm_enabled:
return Action.PERMIT
data_node_value = root.value # type: InstanceNode
data_node_value = root.value # type: Union[Value, ArrayValue, ObjectValue]
nl = self.rule_tree.root # type: List[RuleTreeNode]
node_match = None # type: RuleTreeNode
......@@ -344,10 +355,14 @@ class UserNacm:
def _prune_data_tree(self, node: InstanceNode, root: InstanceNode, ii: InstanceRoute, access: Permission) -> InstanceNode:
if isinstance(node.value, ObjectValue):
# print("obj: {}".format(node.value))
nsel = MemberName("")
nsel = MemberName(name="", ns=None)
mii = ii + [nsel]
for child_key in node.value.keys():
nsel.key = child_key
key_splitted = child_key.split(":", maxsplit=1)
if len(key_splitted) > 1:
nsel.namespace, nsel.name = key_splitted
else:
nsel.namespace, nsel.name = (None, key_splitted[0])
m = nsel.goto_step(node)
# debug_nacm("checking mii {}".format(mii))
......
......@@ -99,8 +99,8 @@ def test_nacm(datastore_1, nacm_datastore_1):
# debug("Node contents: {}".format(datanode.value))
test_ii = data.parse_ii(test_path[0], PathFormat.XPATH)
rule = []
action = nacm_conf.get_user_nacm(test_user).check_data_node_permission(data.get_data_root(), test_ii, test_path[1],
out_matching_rule=rule)
action = nacm_conf.get_user_rules(test_user).check_data_node_permission(data.get_data_root(), test_ii, test_path[1],
out_matching_rule=rule)
assert action == test_path[2]
"""
if action == test_path[2]:
......@@ -114,7 +114,7 @@ def test_nacm(datastore_1, nacm_datastore_1):
test_ii2 = data.parse_ii("/dns-server:dns-server/zones/zone[domain='example.com']", PathFormat.XPATH)
# info("Reading: " + str(test_ii2))
res = nacm_conf.get_user_nacm(test_user).prun_data_tree(data.get_data_root(), test_ii2)
res = nacm_conf.get_user_rules(test_user).prun_data_tree(data.get_data_root(), test_ii2)
res = json.dumps(res.value, indent=4, sort_keys=True)
res_expected = """
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment