stash1

parent 0a1d2601
......@@ -34,8 +34,8 @@ def main():
tm.test()
except ImportError as e:
print(e.msg)
except AttributeError:
print("Module \"{}\" has no test() function".format(test_module))
#except AttributeError:
# print("Module \"{}\" has no test() function".format(test_module))
else:
rest_server.run()
......
......@@ -4,7 +4,7 @@ from threading import Lock
import colorlog
import sys
from enum import Enum, unique
from enum import Enum
from colorlog import error, warning as warn, info, debug
from typing import List, Any, Dict, TypeVar, Tuple, Set
import copy
......@@ -14,109 +14,155 @@ from yangson import DataModel
from yangson.datamodel import InstanceIdentifier
class PathFormat(Enum):
URL = 0
XPATH = 1
class NacmForbiddenError(Exception):
def __init__(self, msg="Access to data node rejected by NACM"):
self.msg = msg
class DataLockError(Exception):
def __init__(self, msg=""):
self.msg = msg
class Rpc:
def __init__(self):
self.username = None # type: str
self.path = None # type: str
self.path_format = PathFormat.URL # type: PathFormat
class BaseDatastore:
def __init__(self, module_dir: str, yang_library_file: str):
self.data = None # type: Instance
self.dm = None # type: DataModel
def __init__(self, module_dir: str, yang_library_file: str, name: str=""):
self.name = name
self.nacm = None # type: NacmConfig
self._data = None # type: Instance
self._dm = None # type: DataModel
self._data_lock = Lock()
self._lock_username = None
self._lock_username = None # type: str
with open(yang_library_file) as ylfile:
yl = ylfile.read()
self.dm = DataModel.from_yang_library(yl, module_dir)
self._dm = DataModel.from_yang_library(yl, module_dir)
# Register NACM module to datastore
def register_nacm(self, nacm_config: "NacmConfig"):
self.nacm = nacm_config
# Returns the root node of data tree
def get_data_root(self) -> Instance:
return self.data
return self._data
# Just get the node, do not evaluate NACM (for testing purposes)
def get_node(self, ii: InstanceIdentifier) -> Instance:
self.lock_data()
n = self.data.goto(ii)
self.unlock_data()
# self.lock_data("get_node")
n = self._data.goto(ii)
# self.unlock_data()
return n
def get_node_path(self, ii_str: str) -> Instance:
ii = self.dm.parse_instance_id(ii_str)
return self.data.goto(ii)
def get_node_rpc(self, rpc: Rpc) -> Instance:
# Just get the node, do not evaluate NACM (for testing purposes)
def get_node_path(self, path: str, path_format: PathFormat) -> Instance:
n = None
ii = self.dm.parse_instance_id(rpc.path)
self.lock_data(rpc.username)
n = self.data.goto(ii)
self.unlock_data()
if self.nacm:
nrpc = NacmRpc(self.nacm, self, None, rpc.username)
if nrpc.check_data_node(n, Permission.NACM_ACCESS_READ) == Action.DENY:
return None
else:
# Prun subtree data
n = nrpc.check_data_read(n)
if path_format == PathFormat.URL:
ii = self._dm.parse_resource_id(path)
else:
ii = self._dm.parse_instance_id(path)
# self.lock_data("get_node_path")
n = self._data.goto(ii)
# self.unlock_data()
return n
def get_node_rpc2(self, rpc: Rpc) -> Instance:
# Get data node, evaluate NACM if possible
def get_node_rpc(self, rpc: Rpc) -> Instance:
n = None
ii = self.dm.parse_resource_id(rpc.path)
self.lock_data(rpc.username)
n = self.data.goto(ii)
self.unlock_data()
if rpc.path_format == PathFormat.URL:
ii = self._dm.parse_resource_id(rpc.path)
else:
ii = self._dm.parse_instance_id(rpc.path)
# self.lock_data(rpc.username)
n = self._data.goto(ii)
# self.unlock_data()
if self.nacm:
nrpc = NacmRpc(self.nacm, self, None, rpc.username)
nrpc = NacmRpc(self.nacm, self, rpc.username)
if nrpc.check_data_node(n, Permission.NACM_ACCESS_READ) == Action.DENY:
return None
raise NacmForbiddenError()
else:
# Prun subtree data
n = nrpc.check_data_read(n)
return n
# Locks datastore data
def lock_data(self, username: str = None):
ret = self._data_lock.acquire(blocking=False)
if ret:
self._lock_username = username or "(unknown)"
info("Acquired data lock for user {}".format(username))
info("Acquired lock in datastore \"{}\" for user \"{}\"".format(self.name, username))
else:
info("Failed to acquire lock for user {}, already locked by {}".format(username, self._lock_username))
return ret
raise DataLockError(
"Failed to acquire lock in datastore \"{}\" for user \"{}\", already locked by \"{}\"".format(
self.name,
username,
self._lock_username
)
)
# Unlocks datastore data
def unlock_data(self):
self._data_lock.release()
info("Released data lock for user {}".format(self._lock_username))
info("Released lock in datastore \"{}\" for user \"{}\"".format(self.name, self._lock_username))
self._lock_username = None
# Loads the data from file
def load(self, filename: str):
raise NotImplementedError("Not implemented in base class")
# Saves the data to file
def save(self, filename: str):
raise NotImplementedError("Not implemented in base class")
class JsonDatastore(BaseDatastore):
def load_json(self, filename: str):
def load(self, filename: str):
self._data = None
with open(filename, "rt") as fp:
self.data = self.dm.from_raw(json.load(fp))
self._data = self._dm.from_raw(json.load(fp))
def save_json(self, filename: str):
def save(self, filename: str):
with open(filename, "w") as jfd:
json.dump(self.data, jfd)
self.lock_data("json_save")
json.dump(self._data, jfd)
self.unlock_data()
def test():
"""
with open("./data/yang-library-data.json") as ylfile:
yl = ylfile.read()
_dm = DataModel.from_yang_library(yl, "./data")
with open("jetconf/example-data.json", "rt") as fp:
_root = _dm.from_raw(json.load(fp))
print(hash(_root.member("dns-server:dns-server").value))
"""
# exit()
data = JsonDatastore("./data", "./data/yang-library-data.json")
data.load_json("jetconf/example-data.json")
data.load("jetconf/example-data.json")
rpc = Rpc()
rpc.username = "dominik"
rpc.path = "/dns-server:dns-server/zones/zone[domain='example.com']/query-module"
rpc.path_format = PathFormat.XPATH
n = data.get_node_rpc(rpc)
print(n.value)
print(hash(data.get_data_root().member("dns-server:dns-server").value))
from .nacm import NacmConfig, NacmRpc, Permission, Action
{
"ietf-netconf-acm:nacm": {
"enable-nacm": true,
"read-default": "permit",
"read-default": "deny",
"write-default": "deny",
"exec-default": "deny",
"denied-operations": 123,
......@@ -62,6 +62,13 @@
"comment": "Users can write other zones.",
"action": "permit"
},
{
"name": "deny-query-module",
"path": "/dns-server:dns-server/zones/zone[domain='example.com']/query-module",
"access-operations": "*",
"comment": "... but no query-module",
"action": "deny"
},
{
"name": "povol pristup ke groups",
"path": "/ietf-netconf-acm:nacm/groups",
......
......@@ -11,7 +11,6 @@ from typing import List, Any, Dict, TypeVar, Tuple, Set
from yangson.instance import Instance, NonexistentInstance, ArrayValue, ObjectValue
from yangson.schema import NonexistentSchemaNode
JsonNodeT = Dict[str, Any]
......@@ -163,7 +162,7 @@ class NacmConfig:
# Rules for particular session (logged-in user)
class NacmRpc:
# "username" only for testing, will be part of "session"
def __init__(self, config: NacmConfig, data: "BaseDatastore", session: Any, username: str):
def __init__(self, config: NacmConfig, data: "BaseDatastore", username: str):
self.default_read = config.default_read
self.default_write = config.default_write
self.default_exec = config.default_exec
......@@ -177,10 +176,31 @@ class NacmRpc:
if not isinstance(node, Instance):
raise TypeError("Node not an Instance!")
root = node.top()
i = 0
print("rule node hashes:")
for rl in self.rule_lists:
for rule in rl.rules:
if rule.type_data.path:
try:
print("{} {} {}".format(i, rule.name, hash(self.data.get_node_path(rule.type_data.path, PathFormat.XPATH))))
i += 1
except NonexistentInstance:
pass
n = node
while not n.is_top():
# debug("checking node {}".format(n.value))
#info("checking node {}".format(n.value))
# try:
info("checking node {} {}".format(hash(n.value), type(n.value)))
# except TypeError as e:
# info("checking node hash error {}".format(type(n.value)))
# info(str(e))
# info(n.value)
for rl in self.rule_lists:
for rule in rl.rules:
debug("Checking rule \"{}\"".format(rule.name))
......@@ -209,8 +229,8 @@ class NacmRpc:
continue
try:
selected = self.data.get_node_path(rule.type_data.path)
if selected.value == n.value:
selected = self.data.get_node_path(rule.type_data.path, PathFormat.XPATH)
if hash(selected.value) == hash(n.value):
# Success!
# the path selects the node
info("Rule found: \"{}\"".format(rule.name))
......@@ -226,6 +246,7 @@ class NacmRpc:
# no rule found
# default action
info("No rule found, returning default action")
if access == Permission.NACM_ACCESS_READ:
return self.default_read
elif access in (Permission.NACM_ACCESS_CREATE, Permission.NACM_ACCESS_DELETE, Permission.NACM_ACCESS_UPDATE):
......@@ -239,7 +260,8 @@ class NacmRpc:
# print("obj: {}".format(node.value))
for child_key in node.value.keys():
# Do not check leaves
if not (isinstance(node.value[child_key], ObjectValue) or isinstance(node.value[child_key], ArrayValue)):
if not (
isinstance(node.value[child_key], ObjectValue) or isinstance(node.value[child_key], ArrayValue)):
continue
m = node.member(child_key)
......@@ -277,15 +299,15 @@ class NacmRpc:
def test():
nacm_data = JsonDatastore("./data", "./data/yang-library-data.json")
nacm_data.load_json("jetconf/example-data-nacm.json")
nacm_data.load("jetconf/example-data-nacm.json")
nacm = NacmConfig(nacm_data)
data = JsonDatastore("./data", "./data/yang-library-data.json")
data.load_json("jetconf/example-data.json")
data.load("jetconf/example-data.json")
data.register_nacm(nacm)
rpc = NacmRpc(nacm, data, None, "dominik")
nrpc = NacmRpc(nacm, data, "dominik")
test_paths = (
(
......@@ -303,11 +325,11 @@ def test():
for test_path in test_paths:
info("Testing path \"{}\"".format(test_path[0]))
datanode = data.get_node_path(test_path[0])
datanode = data.get_node_path(test_path[0], PathFormat.XPATH)
if datanode:
info("Node found")
debug("Node contents: {}".format(datanode.value))
action = rpc.check_data_node(datanode, test_path[1])
action = nrpc.check_data_node(datanode, test_path[1])
if action == test_path[2]:
info("Action = {}, {}\n".format(action.name, "OK"))
else:
......@@ -315,16 +337,37 @@ def test():
else:
info("Node not found!")
exit()
_node = data.get_node_path("/ietf-netconf-acm:nacm/groups")
_node = data.get_node_path("/dns-server:dns-server/zones/zone[domain='example.com']", PathFormat.XPATH)
if not _node:
print("Node null")
res = rpc.check_data_read(_node)
print("result = {}".format(res.value))
if json.loads(json.dumps(res.value)) == {'group': [{'name': 'users', 'user-name': ['lada', 'pavel', 'dominik', 'lojza@mail.cz']}]}:
res = nrpc.check_data_read(_node)
res = json.dumps(res.value, indent=4)
print("result = \n" + res + "\n")
res_expected = """
{
"master": [
"server1"
],
"access-control-list": [
"acl-xfr-update",
"acl-notify"
],
"any-to-tcp": false,
"template": "default",
"notify": {
"recipient": [
"server0"
]
},
"domain": "example.com"
}"""
if json.loads(res) == json.loads(res_expected):
info("OK")
else:
warn("FAILED")
from .data import JsonDatastore
from .data import JsonDatastore, PathFormat
......@@ -4,16 +4,13 @@ import mimetypes
import os
import ssl
from collections import OrderedDict
import sys
import logging
import colorlog
from colorlog import error, warning as warn, info, debug
from typing import List, Tuple, Dict, Any
import yaml
import copy
from .nacm import NacmConfig
from .data import JsonDatastore, Rpc
from .data import JsonDatastore, Rpc, NacmForbiddenError, DataLockError
from yangson.schema import NonexistentSchemaNode
from yangson.instance import NonexistentInstance
from h2.connection import H2Connection
from h2.events import DataReceived, RequestReceived, RemoteSettingsChanged
......@@ -31,6 +28,7 @@ CONFIG = {
"CA_CERT": "ca.pem"
}
# NACM_ADMINS = []
class CertHelpers:
@staticmethod
......@@ -85,7 +83,6 @@ class H2Protocol(asyncio.Protocol):
def handle_get(self, headers: OrderedDict, stream_id: int):
response = None
print(headers[":path"])
if headers[":path"] == CONFIG["RESTCONF_NACM_API_ROOT"]:
# Top level api resource (appendix D.1.1)
......@@ -104,37 +101,6 @@ class H2Protocol(asyncio.Protocol):
self.conn.send_headers(stream_id, response_headers)
self.conn.send_data(stream_id, response.encode(), end_stream=True)
elif headers[":path"].startswith(os.path.join(CONFIG["RESTCONF_NACM_API_ROOT"], "data")):
# NACM api request
info(("nacm_api_get: " + headers[":path"]))
username = CertHelpers.get_field(self.client_cert, "emailAddress")
pth = headers[":path"][len(os.path.join(CONFIG["RESTCONF_NACM_API_ROOT"], "data")):]
rpc1 = Rpc()
rpc1.username = username
rpc1.path = pth
try:
n = ex_datastore.nacm.nacm_ds.get_node_rpc2(rpc1)
response = json.dumps(n.value, indent=4) + "\n"
http_status = "200"
except NonexistentSchemaNode:
warn("Node not found: " + pth)
response = "Not found"
http_status = "404"
response_headers = (
(':status', http_status),
('content-type', 'application/yang.api+json'),
('content-length', len(response)),
('server', CONFIG["SERVER_NAME"]),
)
self.conn.send_headers(stream_id, response_headers)
self.conn.send_data(stream_id, response.encode(), end_stream=True)
elif headers[":path"] == CONFIG["RESTCONF_API_ROOT"]:
# Top level api resource (appendix D.1.1)
response = ("{\n"
......@@ -152,6 +118,45 @@ class H2Protocol(asyncio.Protocol):
self.conn.send_headers(stream_id, response_headers)
self.conn.send_data(stream_id, response.encode(), end_stream=True)
elif headers[":path"].startswith(os.path.join(CONFIG["RESTCONF_NACM_API_ROOT"], "data")):
# NACM api request
info(("nacm_api_get: " + headers[":path"]))
username = CertHelpers.get_field(self.client_cert, "emailAddress")
if username not in NACM_ADMINS:
warn(username + " not allowed to access NACM data")
response = "Forbidden"
http_status = "403"
else:
pth = headers[":path"][len(os.path.join(CONFIG["RESTCONF_NACM_API_ROOT"], "data")):]
rpc1 = Rpc()
rpc1.username = username
rpc1.path = pth
try:
n = ex_datastore.nacm.nacm_ds.get_node_rpc(rpc1)
response = json.dumps(n.value, indent=4) + "\n"
http_status = "200"
except NonexistentSchemaNode:
warn("NonexistentSchemaNode: " + pth)
response = "NonexistentSchemaNode"
http_status = "404"
except NonexistentInstance:
warn("NonexistentInstance: " + pth)
response = "NonexistentInstance"
http_status = "404"
response_headers = (
(':status', http_status),
('content-type', 'application/yang.api+json'),
('content-length', len(response)),
('server', CONFIG["SERVER_NAME"]),
)
self.conn.send_headers(stream_id, response_headers)
self.conn.send_data(stream_id, response.encode(), end_stream=True)
elif headers[":path"].startswith(os.path.join(CONFIG["RESTCONF_API_ROOT"], "data")):
# api request
info(("api_get: " + headers[":path"]))
......@@ -165,13 +170,26 @@ class H2Protocol(asyncio.Protocol):
rpc1.path = pth
try:
n = ex_datastore.get_node_rpc2(rpc1)
ex_datastore.lock_data(username)
n = ex_datastore.get_node_rpc(rpc1)
response = json.dumps(n.value, indent=4) + "\n"
http_status = "200"
except DataLockError as e:
warn(e.msg)
except NacmForbiddenError as e:
warn(e.msg)
response = "Forbidden"
http_status = "403"
except NonexistentSchemaNode:
warn("Node not found: " + pth)
response = "Not found"
warn("NonexistentSchemaNode: " + pth)
response = "NonexistentSchemaNode"
http_status = "404"
except NonexistentInstance:
warn("NonexistentInstance: " + pth)
response = "NonexistentInstance"
http_status = "404"
finally:
ex_datastore.unlock_data()
response_headers = (
(':status', http_status),
......@@ -232,6 +250,7 @@ class H2Protocol(asyncio.Protocol):
def handle_post(self, headers: OrderedDict, data: bytes, stream_id: int):
print("post")
return
parsed_url = yang_json_path.URLPath(headers[":path"])
print(json.dumps({"query": parsed_url.query_table, "path": parsed_url.path_list}, indent=4))
......@@ -255,24 +274,32 @@ class H2Protocol(asyncio.Protocol):
def run():
global ex_datastore
global NACM_ADMINS
try:
with open("jetconf/config.yaml") as conf_fd:
conf_yaml = yaml.load(conf_fd)
CONFIG.update(conf_yaml.get("HTTP_SERVER", {}))
try:
CONFIG.update(conf_yaml["HTTP_SERVER"])
except KeyError:
pass
try:
NACM_ADMINS = conf_yaml["NACM"]["ALLOWED_USERS"]
except KeyError:
pass
except FileNotFoundError:
warn("Configuration file does not exist")
info("Using config:\n" + yaml.dump([CONFIG, ], default_flow_style=False))
global ex_datastore
nacm_data = JsonDatastore("./data", "./data/yang-library-data.json")
nacm_data.load_json("jetconf/example-data-nacm.json")
nacm_data = JsonDatastore("./data", "./data/yang-library-data.json", "NACM data")
nacm_data.load("jetconf/example-data-nacm.json")
nacmc = NacmConfig(nacm_data)
ex_datastore = JsonDatastore("./data", "./data/yang-library-data.json")
ex_datastore.load_json("jetconf/example-data.json")
ex_datastore = JsonDatastore("./data", "./data/yang-library-data.json", "DNS data")
ex_datastore.load("jetconf/example-data.json")
ex_datastore.register_nacm(nacmc)
ssl_context = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH)
......
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