Optimized NACM performance and improved operations support

parent e3ce73b1
......@@ -27,9 +27,10 @@ def main():
ex_datastore = JsonDatastore("./data", "./data/yang-library-data.json", "DNS data")
ex_datastore.load("jetconf/example-data.json")
ex_datastore.register_nacm(nacmc)
nacmc.set_ds(ex_datastore)
# Register op handlers
OP_HANDLERS.register_handler("play", op_handlers.play_op_handler)
OP_HANDLERS.register_handler("generate-key", op_handlers.sign_op_handler)
# Create HTTP server
rest_srv = RestServer()
......
......@@ -8,6 +8,7 @@ from enum import Enum
from colorlog import error, warning as warn, info, debug
from typing import List, Any, Dict, TypeVar, Tuple, Set
from yangson.context import Context
from yangson.datamodel import InstanceIdentifier, DataModel
from yangson.instance import \
Instance, \
......@@ -21,6 +22,7 @@ from yangson.instance import \
EntryIndex
from .helpers import DataHelpers
from .handler_list import OP_HANDLERS
class PathFormat(Enum):
......@@ -33,6 +35,9 @@ class NacmForbiddenError(Exception):
self.msg = msg
self.rulename = rule
def __str__(self):
return self.msg
class DataLockError(Exception):
def __init__(self, msg=""):
......@@ -42,12 +47,23 @@ class DataLockError(Exception):
return self.msg
class NoHandlerForOpError(Exception):
def __init__(self, msg=""):
self.msg = msg
def __str__(self):
return self.msg
class Rpc:
def __init__(self):
self.username = None # type: str
self.path = None # type: str
self.qs = None # type: Dict[str, List[str]]
self.path_format = PathFormat.URL # type: PathFormat
self.path_format = PathFormat.URL # type: PathFormat
self.skip_nacm_check = False # type: bool
self.op_name = None # type: str
self.op_input_args = None # type: ObjectValue
class BaseDatastore:
......@@ -97,7 +113,7 @@ class BaseDatastore:
n = self._data.goto(ii)
if self.nacm:
nrpc = NacmRpc(self.nacm, self, rpc.username)
nrpc = self.nacm.get_user_nacm(rpc.username)
if nrpc.check_data_node_path(ii, Permission.NACM_ACCESS_READ) == Action.DENY:
raise NacmForbiddenError()
else:
......@@ -111,7 +127,7 @@ class BaseDatastore:
n = self._data.goto(ii)
if self.nacm:
nrpc = NacmRpc(self.nacm, self, rpc.username)
nrpc = self.nacm.get_user_nacm(rpc.username)
if nrpc.check_data_node_path(ii, Permission.NACM_ACCESS_CREATE) == Action.DENY:
raise NacmForbiddenError()
......@@ -157,13 +173,12 @@ class BaseDatastore:
else:
raise InstanceTypeError(n, "Child node can only be appended to Object or Array")
def put_node_rpc(self, rpc: Rpc, value: Any):
ii = self.parse_ii(rpc.path, rpc.path_format)
n = self._data.goto(ii)
if self.nacm:
nrpc = NacmRpc(self.nacm, self, rpc.username)
nrpc = self.nacm.get_user_nacm(rpc.username)
if nrpc.check_data_node_path(ii, Permission.NACM_ACCESS_UPDATE) == Action.DENY:
raise NacmForbiddenError()
......@@ -181,7 +196,7 @@ class BaseDatastore:
last_isel = ii[-1]
if self.nacm:
nrpc = NacmRpc(self.nacm, self, rpc.username)
nrpc = self.nacm.get_user_nacm(rpc.username)
if nrpc.check_data_node_path(ii, Permission.NACM_ACCESS_DELETE) == Action.DENY:
raise NacmForbiddenError()
......@@ -194,13 +209,27 @@ class BaseDatastore:
self._data = new_n.top()
def check_operation_rpc(self, rpc: Rpc):
ii = self.parse_ii(rpc.path, rpc.path_format)
def invoke_op_rpc(self, rpc: Rpc) -> ObjectValue:
if self.nacm and (not rpc.skip_nacm_check):
nrpc = self.nacm.get_user_nacm(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))
op_handler = OP_HANDLERS.get_handler(rpc.op_name)
if op_handler is None:
raise NoHandlerForOpError()
schema_addr = Context.path2address(rpc.path)
sn = self._dm.schema.get_schema_descendant(schema_addr)
sn_input = sn.get_child("input")
if sn_input is not None:
print(sn_input)
print(sn_input.ascii_tree(""))
ret_data = op_handler(rpc.op_input_args)
return ret_data
if self.nacm:
nrpc = NacmRpc(self.nacm, self, rpc.username)
if nrpc.check_data_node_path(ii, Permission.NACM_ACCESS_EXEC) == Action.DENY:
raise NacmForbiddenError("Op \"{}\" invocation denied for user \"{}\"".format(rpc.path, rpc.username))
# Locks datastore data
def lock_data(self, username: str = None, blocking: bool=True):
......@@ -269,4 +298,4 @@ def test():
else:
warn("FAILED")
from .nacm import NacmConfig, NacmRpc, Permission, Action
from .nacm import NacmConfig, Permission, Action
......@@ -15,6 +15,12 @@
"root"
]
},
{
"name": "gr-pavel",
"user-name": [
"pavel"
]
},
{
"name": "users",
"user-name": [
......@@ -42,6 +48,21 @@
}
]
},
{
"name": "pavel-acl",
"group": [
"gr-pavel"
],
"rule": [
{
"name": "deny-template",
"path": "/dns-server:dns-server/zones/zone[domain='example.com']/template",
"access-operations": "*",
"comment": "... but no template",
"action": "deny"
}
]
},
{
"name": "users-acl",
"group": [
......
......@@ -11,8 +11,7 @@ from yangson.instance import NonexistentInstance, InstanceTypeError, DuplicateMe
from .config import CONFIG_GLOBAL, CONFIG_HTTP, NACM_ADMINS, API_ROOT_data, API_ROOT_ops
from .helpers import CertHelpers, DataHelpers, DateTimeHelpers, ErrorHelpers
from .data import BaseDatastore, Rpc, DataLockError, NacmForbiddenError
from .handler_list import OP_HANDLERS
from .data import BaseDatastore, Rpc, DataLockError, NacmForbiddenError, NoHandlerForOpError
QueryStrT = Dict[str, List[str]]
epretty = ErrorHelpers.epretty
......@@ -362,7 +361,8 @@ def create_api_op(ds: BaseDatastore):
info("invoke_op: " + headers[":path"])
data_str = data.decode("utf-8")
op_name_fq = headers[":path"][len(API_ROOT_ops) + 1:]
api_pth = headers[":path"][len(API_ROOT_ops):]
op_name_fq = api_pth[1:]
op_name_splitted = op_name_fq.split(":", maxsplit=1)
if len(op_name_splitted) < 2:
......@@ -384,40 +384,41 @@ def create_api_op(ds: BaseDatastore):
input_args = json_data.get(ns + ":input")
op_handler = OP_HANDLERS.get_handler(op_name)
if op_handler:
try:
# Skip NACM check for privileged users
if username not in NACM_ADMINS:
rpc1 = Rpc()
rpc1.username = username
rpc1.path = "/" + op_name_fq
ds.check_operation_rpc(rpc1)
ret_data = op_handler(input_args)
if ret_data is None:
prot.send_empty(stream_id, "204", "No Content", False)
else:
response = json.dumps(ret_data, indent=4) + "\n"
response_bytes = response.encode()
response_headers = (
(':status', '200'),
('content-type', 'application/yang.api+json'),
('content-length', len(response_bytes)),
('server', CONFIG_HTTP["SERVER_NAME"]),
)
prot.conn.send_headers(stream_id, response_headers)
prot.conn.send_data(stream_id, response_bytes, end_stream=True)
except NacmForbiddenError as e:
warn(epretty(e))
prot.send_empty(stream_id, "403", "Forbidden")
except NonexistentSchemaNode as e:
warn(epretty(e))
prot.send_empty(stream_id, "404", "Not Found")
else:
rpc1 = Rpc()
rpc1.username = username
rpc1.path = api_pth
rpc1.op_name = op_name
rpc1.op_input_args = input_args
# Skip NACM check for privileged users
if username in NACM_ADMINS:
rpc1.skip_nacm_check = True
try:
ret_data = ds.invoke_op_rpc(rpc1)
if ret_data is None:
prot.send_empty(stream_id, "204", "No Content", False)
else:
response = json.dumps(ret_data, indent=4) + "\n"
response_bytes = response.encode()
response_headers = (
(':status', '200'),
('content-type', 'application/yang.api+json'),
('content-length', len(response_bytes)),
('server', CONFIG_HTTP["SERVER_NAME"]),
)
prot.conn.send_headers(stream_id, response_headers)
prot.conn.send_data(stream_id, response_bytes, end_stream=True)
except NacmForbiddenError as e:
warn(epretty(e))
prot.send_empty(stream_id, "403", "Forbidden")
except NonexistentSchemaNode as e:
warn(epretty(e))
prot.send_empty(stream_id, "404", "Not Found")
except NoHandlerForOpError:
warn("Nonexistent handler for operation \"{}\"".format(op_name))
prot.send_empty(stream_id, "400", "Bad Request")
......
This diff is collapsed.
......@@ -7,3 +7,10 @@ def play_op_handler(input_args: JsonNodeT) -> JsonNodeT:
print("Playing song {} in playlist \"{}\"".format(input_args.get("song-number"), input_args.get("playlist")))
ret = {"status": "OK"}
return ret
def sign_op_handler(input_args: JsonNodeT) -> JsonNodeT:
print("input={}".format(input_args))
print("signing...")
ret = {"status": "OK"}
return ret
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