diff --git a/src/p4p.h b/src/p4p.h index 285a499b..298f1fa8 100644 --- a/src/p4p.h +++ b/src/p4p.h @@ -3,6 +3,8 @@ #include #include +#include +#include #include #include @@ -50,6 +52,42 @@ namespace p4p { using namespace pvxs; +namespace detail { + +template +static inline auto credAuthorityImpl(const C& c, int) -> decltype(std::declval().authority, std::string()) +{ + return c.authority; +} + +template +static inline std::string credAuthorityImpl(const C& c, long) +{ + try { + auto fld = c.raw["authority"]; + if(fld.storageType()==StoreType::String) + return fld.template as(); + }catch(...){ + } + return std::string(); +} + +template +static inline auto credProtocolImpl(const C& c, int) -> decltype(std::declval().isTLS, std::string()) +{ + return c.isTLS ? std::string("TLS") : std::string("TCP"); +} + +template +static inline std::string credProtocolImpl(const C& c, long) +{ + if(c.method=="x509") + return "TLS"; + return "TCP"; +} + +} + typedef epicsGuard Guard; typedef epicsGuardRelease UnGuard; @@ -176,6 +214,32 @@ struct PyLock ~PyLock() { PyGILState_Release(state); } }; +// HACK!!!!! +inline +std::string assembleCred(const server::ClientCredentials& cred) { + const auto& method = cred.method; + if(method=="ca") { + auto sep = cred.account.find_last_of('/'); + if(sep==cred.account.npos) { + return cred.account; + } else { + return cred.account.substr(sep+1); + } + } else { + return method + "/" + cred.account; + } +} + +inline +std::string credAuthority(const server::ClientCredentials& cred) { + return detail::credAuthorityImpl(cred, 0); +} + +inline +std::string credProtocol(const server::ClientCredentials& cred) { + return detail::credProtocolImpl(cred, 0); +} + TypeDef startPrototype(const std::string& id, const Value& base); void appendPrototype(TypeDef& def, PyObject* spec); PyObject* asPySpec(const Value& v, bool fakearray=false); diff --git a/src/p4p/_gw.pyx b/src/p4p/_gw.pyx index fc4e5383..8f4895bf 100644 --- a/src/p4p/_gw.pyx +++ b/src/p4p/_gw.pyx @@ -17,8 +17,14 @@ from libcpp.vector cimport vector from .pvxs.client cimport Context, Report, ReportInfo from .pvxs.server cimport ServerGUID from .pvxs.source cimport Source, ChannelControl, OpBase, ClientCredentials +from .pvxs cimport source from . cimport _p4p +cdef extern from "" namespace "p4p": + string assembleCred(const source.ClientCredentials&) except+ + string credAuthority(const source.ClientCredentials&) except+ + string credProtocol(const source.ClientCredentials&) except+ + cdef extern from "pvxs_gw.h" namespace "p4p" nogil: enum: GWSearchIgnore enum: GWSearchClaim @@ -79,7 +85,38 @@ cdef class InfoBase(object): @property def account(self): if self.info: - return self.info.get().account.decode('UTF-8') + return assembleCred(self.info.get()[0]).decode('UTF-8') + else: + return u'' + + @property + def accountname(self): + if self.info: + acct = self.info.get().account.decode('UTF-8', 'replace') + if self.info.get().method == b'ca' and '/' in acct: + acct = acct.rsplit('/', 1)[-1] + return acct + else: + return u'' + + @property + def method(self): + if self.info: + return self.info.get().method.decode('UTF-8', 'replace') + else: + return u'' + + @property + def authority(self): + if self.info: + return credAuthority(self.info.get()[0]).decode('UTF-8', 'replace') + else: + return u'' + + @property + def protocol(self): + if self.info: + return credProtocol(self.info.get()[0]).decode('UTF-8', 'replace') else: return u'' diff --git a/src/p4p/_p4p.pyx b/src/p4p/_p4p.pyx index 56851062..17fe74ad 100644 --- a/src/p4p/_p4p.pyx +++ b/src/p4p/_p4p.pyx @@ -30,6 +30,8 @@ cdef extern from "" namespace "p4p": # p4p.h redefines/overrides some definitions from Python.h (eg. PyMODINIT_FUNC) # it also (re)defines macros effecting numpy/arrayobject.h + string assembleCred(const source.ClientCredentials&) except+ + # pvxs_type.cpp data.TypeDef startPrototype(const string& id, const data.Value& base) except+ void appendPrototype(data.TypeDef&, object spec) except+ @@ -866,7 +868,7 @@ cdef class ServerOperation: '''account() -> str Client identity ''' - return self.op.get().credentials().get().account.decode() + return assembleCred(self.op.get().credentials().get()[0]).decode() def roles(self): '''roles() -> {str} diff --git a/src/p4p/asLib/__init__.py b/src/p4p/asLib/__init__.py index d5e02174..c2f43456 100644 --- a/src/p4p/asLib/__init__.py +++ b/src/p4p/asLib/__init__.py @@ -1,4 +1,5 @@ import logging +import logging import warnings import socket import re @@ -11,7 +12,8 @@ from .yacc import parse, ACFError from .. import Value -from ..client.thread import Context, Disconnected +from ..client.thread import Context +from ..client import raw as _raw _log = logging.getLogger(__name__) @@ -71,6 +73,18 @@ def report(self): def parse(self, acf): ast = parse(acf) + auth_ids_by_cn = defaultdict(set) + auth_cn_by_id = {} + + def _auth_walk(node): + _, aid, cn, kids = node + if aid is not None: + auth_cn_by_id[aid] = cn + auth_ids_by_cn[cn].add(aid) + for kid in kids or []: + if kid is not None: + _auth_walk(kid) + # map user or host to set of groups uag = defaultdict(set) hag = defaultdict(set) @@ -82,16 +96,20 @@ def parse(self, acf): invars = defaultdict(list) for node in ast: + if node[0] == 'AUTHDEF': + _auth_walk(node) + continue + if node[0]=='UAG': # ('UAG', name, [members...]) uags.add(node[1]) - for member in node[2]: + for member in (node[2] or []): uag[member].add(node[1]) elif node[0]=='HAG': # ('HAG', name, [members...]) hags.add(node[1]) - for member in node[2]: + for member in (node[2] or []): hag[member].add(node[1]) elif node[0]=='ASG': @@ -100,10 +118,10 @@ def parse(self, acf): # | ('RULE', 1, 'WRITE', trap, None | []) rules, inputs = asg[node[1]] = [], {} - for anode in node[2]: + for anode in (node[2] or []): if anode[0]=='RULE': rule = [] - for rnode in anode[4] or []: + for rnode in (anode[4] or []): if rnode[0] in ('UAG', 'HAG'): # ('UAG', ['name']) # ('HAG', ['name']) @@ -125,8 +143,25 @@ def parse(self, acf): rule.append((rnode[0], rnode[1], expr)) + elif rnode[0] in ('METHOD', 'AUTHORITY'): + # ('METHOD', ['x509', ...]) + # ('AUTHORITY', ['AuthName', ...]) + if rnode[0] == 'METHOD': + rule.append((rnode[0], set([(m or '').lower() for m in (rnode[1] or [])]))) + else: + rule.append((rnode[0], set(rnode[1] or []))) + + elif rnode[0]=='PROTOCOL': + # ('PROTOCOL', 'TLS' | 'TCP' | ...) + rule.append((rnode[0], (rnode[1] or '').upper())) + + elif rnode[0]=='UNKNOWN': + # Any unknown predicate disables the RULE (fail-secure) + rule.append((rnode[0], rnode[1])) + else: warnings.warn("Invalid RULE condition AST: %s"%(rnode,)) + rule.append(('UNKNOWN', rnode[0])) try: mask = actionmask[anode[2]] @@ -168,6 +203,8 @@ def parse(self, acf): self._asg = asg self._asg_DEFAULT = asg.get('DEFAULT', []) self._hag_addr = hag_addr + self._auth_ids_by_cn = dict(auth_ids_by_cn) + self._auth_cn_by_id = auth_cn_by_id self._recompute() @@ -182,7 +219,7 @@ def parse(self, acf): def _var_update(self, grps, value): # clear old value first val = None - if not isinstance(value, Disconnected): + if not isinstance(value, _raw.Disconnected): try: val = float(value or 0.0) except: @@ -202,11 +239,21 @@ def _recompute(self, only=None): _log.debug("Recompute %s", only or "all") anodes, self._anodes = self._anodes, WeakKeyDictionary() - for channel, (group, user, host, level) in anodes.items(): + for channel, info in anodes.items(): + group, user, host, level = info[:4] + roles = info[4] if len(info) > 4 else None + method = info[5] if len(info) > 5 else None + authority = info[6] if len(info) > 6 else None + protocol = info[7] if len(info) > 7 else None + if only is None or group in only: - self.create(channel, group, user, host, level) + self.create(channel, group, user, host, level, + roles=roles, + method=method, + authority=authority, + protocol=protocol) else: - self._anodes[channel] = (group, user, host, level) + self._anodes[channel] = info @staticmethod def _gethostbyname(host): @@ -233,13 +280,34 @@ def resolve_hag(self): self._recompute() - def create(self, channel, group, user, host, level, roles=[]): + def create(self, channel, group, user, host, level, roles=None, method=None, authority=None, protocol=None): # Default to restrictive. Used in case of error perm = 0 _log.debug('(re)create %s, %s, %s, %s, %s', channel.name, group, user, host, level) with self._lock: + if method == 'ca' and user is not None and '/' in user: + user = user.rsplit('/', 1)[-1] + + if roles is None: + roles = [] + + authset = set() + if authority: + if isinstance(authority, (list, tuple, set)): + for ent in authority: + if ent: + authset.add(ent) + else: + for ent in str(authority).splitlines(): + ent = ent.strip() + if ent: + authset.add(ent) + + for cn in list(authset): + authset.update(self._auth_ids_by_cn.get(cn, ())) + uags = self._uag.get(user, set()) for role in roles: uags |= self._uag.get('role/'+role, set()) @@ -255,6 +323,14 @@ def create(self, channel, group, user, host, level, roles=[]): accept = len(cond[1].intersection(uags)) elif cond[0]=='HAG': accept = len(cond[1].intersection(hags)) + elif cond[0]=='METHOD': + accept = (method is not None) and (method.lower() in cond[1]) + elif cond[0]=='AUTHORITY': + accept = bool(authset.intersection(cond[1])) + elif cond[0]=='PROTOCOL': + accept = (protocol is not None) and (str(protocol).upper() == cond[1]) + elif cond[0]=='UNKNOWN': + accept = False elif cond[0]=='CALC': try: accept = float(eval(cond[2], {}, inputs) or 0.0) >= 0.5 # horray for legacy... I mean compatibility @@ -286,7 +362,8 @@ def create(self, channel, group, user, host, level, roles=[]): channel.access(put=bool(put), rpc=bool(rpc), uncached=bool(uncached), audit=trapit) - self._anodes[channel] = (group, user, host, level) + self._anodes[channel] = (group, user, host, level, + list(roles), method, authority, protocol) def _check_host(self, hag, user, host): groups = self._hag_addr.get(host) or set() diff --git a/src/p4p/asLib/lex.py b/src/p4p/asLib/lex.py index 8a7ae70c..a905d061 100644 --- a/src/p4p/asLib/lex.py +++ b/src/p4p/asLib/lex.py @@ -9,8 +9,12 @@ class ACFError(RuntimeError): 'ASG', 'RULE', 'CALC', + 'METHOD', + 'AUTHORITY', + 'PROTOCOL', 'INP', 'INTEGER', + 'FLOAT', 'STRING', ) @@ -23,25 +27,33 @@ def t_eol(t): t.lexer.lineno += len(t.value) def t_comment(t): - r'\#[^\n]*\n' - t.lexer.lineno += 1 + r'\#[^\n]*' + # newline(s) consumed by t_eol + pass def t_KW(t): - r'UAG|HAG|ASG|RULE|CALC' + r'UAG|HAG|ASG|RULE|CALC|METHOD|AUTHORITY|PROTOCOL' t.type = t.value return t def t_INP(t): - r'INP[A-L]' + # EPICS Base allows INPA..INPU + r'INP[A-U]' t.type = 'INP' return t def t_INTEGER(t): - r'[0-9]+' + r'[-+]?[0-9]+' t.type = 'INTEGER' t.value = int(t.value) return t +def t_FLOAT(t): + r'[-+]?[0-9]*\.[0-9]+([eE][-+]?[0-9]+)?' + t.type = 'FLOAT' + t.value = float(t.value) + return t + def t_bare_STRING(t): r'[a-zA-Z0-9_\-+:.\[\]<>;]+' t.type = 'STRING' @@ -59,5 +71,5 @@ def t_error(t): if __name__=='__main__': import logging logging.basicConfig(level=logging.DEBUG) - lexer = lex.lex(debug=1, optimize=0, debuglog=logging.getLogger(__name__)) + lexer = lex.lex(debuglog=logging.getLogger(__name__)) lex.runmain(lexer=lexer) diff --git a/src/p4p/asLib/pvlist.py b/src/p4p/asLib/pvlist.py index d69fba1a..7c299979 100644 --- a/src/p4p/asLib/pvlist.py +++ b/src/p4p/asLib/pvlist.py @@ -105,6 +105,8 @@ def __init__(self, pvl): asl = int(parts[1] if len(parts)>1 else '0') allow[pattern] = (None, asg, asl) + if C.groups!=0: + raise RuntimeError('capture groups not permitted with ALLOW %r. Consider switch to ALIAS or non-capture group.'%pattern) else: raise RuntimeError("Unknown command: %s"%cmd) diff --git a/src/p4p/asLib/yacc.py b/src/p4p/asLib/yacc.py index c0d08add..18867f8a 100644 --- a/src/p4p/asLib/yacc.py +++ b/src/p4p/asLib/yacc.py @@ -1,101 +1,426 @@ -import warnings from .lex import tokens, ACFError start = 'asconfig' -def p_item(p): - """asconfig_item : uag - | hag - | asg - rule_item : uag_ref - | hag_ref - | calc_head + +def _append(dst, item): + if item is not None: + dst.append(item) + return dst + + +def p_asconfig_append(p): + """asconfig : asconfig asconfig_item""" + _append(p[1], p[2]) + p[0] = p[1] + + +def p_asconfig_one(p): + """asconfig : asconfig_item""" + if p[1] is None: + p[0] = [] + else: + p[0] = [p[1]] + + +def p_asconfig_item(p): + """asconfig_item : uag_def + | hag_def + | asg_def + | authority_def + | generic_top_level_item """ + # generic_* and authority_def are ignored p[0] = p[1] -def p_top_items(p): - """uag : UAG '(' STRING ')' string_body - hag : HAG '(' STRING ')' string_body - asg : ASG '(' STRING ')' asg_body + +def p_uag_def(p): + """uag_def : UAG uag_head uag_body_opt""" + p[0] = ('UAG', p[2], p[3]) + + +def p_hag_def(p): + """hag_def : HAG hag_head hag_body_opt""" + p[0] = ('HAG', p[2], p[3]) + + +def p_asg_def(p): + """asg_def : ASG asg_head asg_body_opt""" + p[0] = ('ASG', p[2], p[3]) + + +def p_uag_head(p): + """uag_head : '(' STRING ')'""" + p[0] = p[2] + + +def p_hag_head(p): + """hag_head : '(' STRING ')'""" + p[0] = p[2] + + +def p_asg_head(p): + """asg_head : '(' STRING ')'""" + p[0] = p[2] + + +def p_uag_body_opt_empty(p): + """uag_body_opt :""" + p[0] = None + + +def p_hag_body_opt_empty(p): + """hag_body_opt :""" + p[0] = None + + +def p_asg_body_opt_empty(p): + """asg_body_opt :""" + p[0] = None + + +def p_uag_body_opt(p): + """uag_body_opt : '{' string_list '}'""" + p[0] = p[2] + + +def p_hag_body_opt(p): + """hag_body_opt : '{' string_list '}'""" + p[0] = p[2] + + +def p_asg_body_opt(p): + """asg_body_opt : '{' asg_body_list '}'""" + p[0] = p[2] + + +def p_string_list_append(p): + """string_list : string_list ',' STRING""" + p[1].append(p[3]) + p[0] = p[1] + + +def p_string_list_one(p): + """string_list : STRING""" + p[0] = [p[1]] + + +def p_asg_body_list_append(p): + """asg_body_list : asg_body_list asg_body_item""" + _append(p[1], p[2]) + p[0] = p[1] + + +def p_asg_body_list_one(p): + """asg_body_list : asg_body_item""" + if p[1] is None: + p[0] = [] + else: + p[0] = [p[1]] + + +def p_asg_body_item(p): + """asg_body_item : inp_config + | rule_config + | generic_asg_item """ - p[0] = (p[1], p[3], p[5]) + p[0] = p[1] + + +def p_inp_config(p): + """inp_config : INP '(' STRING ')'""" + # token value is e.g. 'INPA' + p[0] = ('INP', p[1][-1:], p[3]) + -def p_list_append(p): - """asconfig : asconfig asconfig_item - string_list : string_list ',' STRING - asg_list : asg_list asg_item - rule_list : rule_list rule_item +def p_rule_config(p): + """rule_config : RULE rule_head rule_body_opt""" + level, perm, trap = p[2] + p[0] = ('RULE', level, perm, trap, p[3]) + + +def p_rule_head(p): + """rule_head : '(' INTEGER ',' STRING trap_opt ')' + | '(' INTEGER ',' STRING ')' """ - #p[0] = p[1] + [p[len(p)-1]] - p[1].append(p[len(p)-1]) + # NOTE: EPICS Base allows a log option string. We currently only accept TRAPWRITE/NOTRAPWRITE. + if len(p) == 7: + p[0] = (p[2], p[4], p[5]) + else: + p[0] = (p[2], p[4], False) + + +def p_trap_opt_empty(p): + """trap_opt :""" + p[0] = False + + +def p_trap_opt(p): + """trap_opt : ',' STRING""" + if p[2] not in ('TRAPWRITE', 'NOTRAPWRITE'): + raise ACFError("Log options must be TRAPWRITE or NOTRAPWRITE") + p[0] = (p[2] == 'TRAPWRITE') + + +def p_rule_body_opt_empty(p): + """rule_body_opt :""" + p[0] = None + + +def p_rule_body_opt(p): + """rule_body_opt : '{' rule_list '}'""" + p[0] = p[2] + + +def p_rule_list_append(p): + """rule_list : rule_list rule_item""" + _append(p[1], p[2]) p[0] = p[1] -def p_list_one(p): - """asconfig : asconfig_item - string_list : STRING - asg_list : asg_item - rule_list : rule_item + +def p_rule_list_one(p): + """rule_list : rule_item""" + if p[1] is None: + p[0] = [] + else: + p[0] = [p[1]] + + +def p_rule_item(p): + """rule_item : uag_ref + | hag_ref + | calc_ref + | method_ref + | authority_ref + | protocol_ref + | rule_generic_block_elem """ + p[0] = p[1] + + +def p_uag_ref(p): + """uag_ref : UAG '(' string_list ')'""" + p[0] = ('UAG', p[3]) + + +def p_hag_ref(p): + """hag_ref : HAG '(' string_list ')'""" + p[0] = ('HAG', p[3]) + + +def p_calc_ref(p): + """calc_ref : CALC '(' STRING ')'""" + p[0] = ('CALC', p[3]) + + +def p_method_ref(p): + """method_ref : METHOD '(' string_list ')'""" + p[0] = ('METHOD', p[3]) + + +def p_authority_ref(p): + """authority_ref : AUTHORITY '(' string_list ')'""" + p[0] = ('AUTHORITY', p[3]) + + +def p_protocol_ref(p): + """protocol_ref : PROTOCOL '(' STRING ')'""" + p[0] = ('PROTOCOL', p[3]) + + +# --- AUTHORITY definitions (top-level) --- + + +def p_authority_def(p): + """authority_def : AUTHORITY auth_head auth_body_opt""" + aid, cn = p[2] + p[0] = ('AUTHDEF', aid, cn, p[3] or []) + + +def p_auth_head(p): + """auth_head : '(' STRING ',' STRING ')' + | '(' STRING ')' + """ + # Return (id, cn) or (None, cn) + if len(p) == 6: + p[0] = (p[2], p[4]) + else: + p[0] = (None, p[2]) + + +def p_auth_body_opt_empty(p): + """auth_body_opt :""" + p[0] = None + + +def p_auth_body_opt(p): + """auth_body_opt : '{' auth_body_item_list '}'""" + p[0] = p[2] + + +def p_auth_body_item_list_append(p): + """auth_body_item_list : auth_body_item auth_body_item_list""" + # order doesn't matter; keep list anyway + p[2].append(p[1]) + p[0] = p[2] + + +def p_auth_body_item_list_one(p): + """auth_body_item_list : auth_body_item""" p[0] = [p[1]] -def p_head(p): - """uag_ref : UAG '(' string_list ')' - hag_ref : HAG '(' string_list ')' - calc_head : CALC '(' STRING ')' + +def p_auth_body_item(p): + """auth_body_item : AUTHORITY auth_head auth_body_opt + | AUTHORITY auth_head """ - p[0] = (p[1], p[3]) + aid, cn = p[2] + if len(p) == 4: + kids = p[3] or [] + else: + kids = [] + p[0] = ('AUTHDEF', aid, cn, kids) + + +# --- Generic / future-proof syntax (parsed then ignored) --- + -def p_body_empty(p): - """string_body : - asg_body : - rule_body : +def p_keyword(p): + """keyword : UAG + | HAG + | CALC + | METHOD + | AUTHORITY + | PROTOCOL + | non_rule_keyword """ + # Yield a string (used by generic constructs) + p[0] = p[1] + + +def p_non_rule_keyword(p): + """non_rule_keyword : ASG + | RULE + | INP + """ + # INP token carries value like INPA + p[0] = p[1] + + +def p_generic_top_level_item(p): + """generic_top_level_item : STRING generic_head generic_list_block + | STRING generic_head generic_block + | STRING generic_head + """ + # ignore silently (syntax must still be correct) p[0] = None -def p_body(p): - """string_body : '{' string_list '}' - asg_body : '{' asg_list '}' - rule_body : '{' rule_list '}' + +def p_generic_asg_item(p): + """generic_asg_item : STRING generic_head generic_list_block + | STRING generic_head generic_block + | STRING generic_head """ - p[0] = p[2] + p[0] = None + -def p_asg_inp(p): - """asg_item : INP '(' STRING ')' +def p_rule_generic_block_elem(p): + """rule_generic_block_elem : rule_generic_block_elem_name generic_head generic_block + | rule_generic_block_elem_name generic_head """ - p[0] = ('INP', p[1][-1:], p[3]) + # Unknown predicate disables this RULE (fail-secure) + p[0] = ('UNKNOWN', p[1]) -def p_asg_rule(p): - """asg_item : RULE '(' INTEGER ',' STRING trap ')' rule_body + +def p_rule_generic_block_elem_name(p): + """rule_generic_block_elem_name : non_rule_keyword + | STRING """ - p[0] = ('RULE', p[3], p[5], p[6], p[8]) + p[0] = p[1] + -def p_trap_empty(p): - """trap : +def p_generic_head(p): + """generic_head : '(' ')' + | '(' generic_element ')' + | '(' generic_list ')' """ - p[0] = False + p[0] = None + + +def p_generic_list_block(p): + """generic_list_block : '{' generic_element '}' '{' generic_list '}'""" + p[0] = None -def p_trap(p): - """trap : ',' STRING + +def p_generic_list_append(p): + """generic_list : generic_list ',' generic_element""" + p[0] = None + + +def p_generic_list_one(p): + """generic_list : generic_element ',' generic_element""" + p[0] = None + + +def p_generic_element(p): + """generic_element : keyword + | STRING + | INTEGER + | FLOAT """ - if p[2] not in ('TRAPWRITE', 'NOTRAPWRITE'): - warnings.warn("trap spec. must be 'TRAPWRITE' or 'NOTRAPWRITE'") - p[0] = False - else: - p[0] = p[2]=='TRAPWRITE' + p[0] = None + + +def p_generic_block(p): + """generic_block : '{' generic_element '}' + | '{' generic_list '}' + | '{' generic_block_list '}' + """ + p[0] = None + + +def p_generic_block_list_append(p): + """generic_block_list : generic_block_list generic_block_elem""" + p[0] = None + + +def p_generic_block_list_one(p): + """generic_block_list : generic_block_elem""" + p[0] = None + + +def p_generic_block_elem(p): + """generic_block_elem : generic_block_elem_name generic_head generic_block + | generic_block_elem_name generic_head + """ + p[0] = None + + +def p_generic_block_elem_name(p): + """generic_block_elem_name : keyword + | STRING + """ + p[0] = None + def p_error(p): - raise ACFError("Syntax error on line %d at or before '%s'"%(p.lineno, p.value)) + if p is None: + raise ACFError("Syntax error at end of input") + raise ACFError("Syntax error on line %d at or before '%s'" % (p.lineno, p.value)) + def parse(acf, debug=False): from ply import yacc, lex from . import lex as _lex - lex = lex.lex(module=_lex) - parser = yacc.yacc(debug=debug,write_tables=False) - return parser.parse(acf, lex, debug=debug) + lexer = lex.lex(module=_lex) + parser = yacc.yacc(debug=debug, write_tables=False) + return parser.parse(acf, lexer, debug=debug) -if __name__=='__main__': + +if __name__ == '__main__': import sys from pprint import pprint - with open(sys.argv[1],'r') as F: + + with open(sys.argv[1], 'r') as F: data = F.read() pprint(parse(data, debug=True)) diff --git a/src/p4p/gw.py b/src/p4p/gw.py index 14a682df..c68c2729 100644 --- a/src/p4p/gw.py +++ b/src/p4p/gw.py @@ -408,7 +408,26 @@ def makeChannel(self, op): try: if not self.readOnly: # default is RO - self.acf.create(chan, asg, op.account, peer, asl, op.roles) + user = getattr(op, 'accountname', None) or op.account + method = getattr(op, 'method', None) + authority = getattr(op, 'authority', None) + protocol = getattr(op, 'protocol', None) + + if method in (None, u''): + try: + acct = op.account + if '/' in acct: + method, user = acct.split('/', 1) + except Exception: + pass + + if protocol in (None, u'') and method == 'x509': + protocol = 'TLS' + + self.acf.create(chan, asg, user, peer, asl, op.roles, + method=method or None, + authority=authority or None, + protocol=protocol or None) if self.getholdoff is not None: chan.access(holdoff=self.getholdoff) except: @@ -450,7 +469,15 @@ def asTest(self, op, pv=None, user=None, peer=None, roles=[]): raise RemoteError("Denied") chan=TestChannel('') - self.acf.create(chan, asg, user, peer, asl, roles) + + method = None + if '/' in user: + method, user = user.split('/', 1) + protocol = 'TLS' if method == 'x509' else None + + self.acf.create(chan, asg, user, peer, asl, roles, + method=method, + protocol=protocol) return permissionsType({ 'pv':pv, @@ -521,6 +548,7 @@ class App(object): def __init__(self, args): _log.info( '*** Gateway STARTS now using "%s".'%args.config) + conf_dir = os.path.abspath(os.path.dirname(args.config)) args._all_config_files = [args.config] with open(args.config, 'r') as F: jconf = F.read() @@ -556,6 +584,8 @@ def __init__(self, args): client_conf['EPICS_PVA_BROADCAST_PORT'] = str(jcli['bcastport']) if 'serverport' in jcli: client_conf['EPICS_PVA_SERVER_PORT'] = str(jcli['serverport']) + if 'tls_keychain' in jcli: + client_conf['EPICS_PVA_TLS_KEYCHAIN'] = os.path.join(conf_dir, str(jcli['tls_keychain'])) for k,v in jcli.items(): # pass through if k.startswith('EPICS_PVA_'): client_conf[k] = v @@ -629,6 +659,8 @@ def __init__(self, args): server_conf['EPICS_PVAS_BROADCAST_PORT'] = str(jsrv['bcastport']) if 'serverport' in jsrv: server_conf['EPICS_PVAS_SERVER_PORT'] = str(jsrv['serverport']) + if 'tls_keychain' in jsrv: + server_conf['EPICS_PVAS_TLS_KEYCHAIN'] = os.path.join(conf_dir, str(jsrv['tls_keychain'])) for k,v in jsrv.items(): # pass through if k.startswith('EPICS_PVA'): server_conf[k] = v diff --git a/src/p4p/test/test_asLib.py b/src/p4p/test/test_asLib.py index 22ea59df..4dd3881a 100644 --- a/src/p4p/test/test_asLib.py +++ b/src/p4p/test/test_asLib.py @@ -142,6 +142,58 @@ def test_parse(self): ]), ]) + def test_parse_epics_7010_extensions(self): + inp = r''' + # Global AUTHORITY definitions (parsed/ignored by p4p) + AUTHORITY(ROOT, "CN=Root") { + AUTHORITY(LEAF, "CN=Leaf") { + AUTHORITY("CN=Only") + } + } + + # Unknown top-level item (parsed/ignored) + FUTURETOP(x) { y(z) { a(b) } } + + ASG(DEFAULT) { + INPU("pv:name") + FUTUREASG(x) { foo(bar) } + + RULE(1,WRITE) { + METHOD(x509, password) + AUTHORITY(ROOT) + PROTOCOL(TLS) + } + + RULE(1,READ) { + FUTUREPRED(x) { foo(bar) } + } + } + ''' + + ast = parse_acf(inp) + + asg_nodes = [n for n in ast if n[0] == 'ASG'] + self.assertEqual(len(asg_nodes), 1) + self.assertEqual(asg_nodes[0][1], 'DEFAULT') + + items = asg_nodes[0][2] + self.assertIn(('INP', 'U', 'pv:name'), items) + + # Find the WRITE rule and verify it contains the new predicates + write_rules = [it for it in items if it[0] == 'RULE' and it[2] == 'WRITE'] + self.assertEqual(len(write_rules), 1) + write_rule = write_rules[0] + conds = write_rule[4] + self.assertIn(('METHOD', ['x509', 'password']), conds) + self.assertIn(('AUTHORITY', ['ROOT']), conds) + self.assertIn(('PROTOCOL', 'TLS'), conds) + + # Unknown predicates in a RULE body must still parse + read_rules = [it for it in items if it[0] == 'RULE' and it[2] == 'READ'] + self.assertEqual(len(read_rules), 1) + read_rule = read_rules[0] + self.assertIn(('UNKNOWN', 'FUTUREPRED'), read_rule[4]) + class TestACL(unittest.TestCase): class DummyChannel(object): def __init__(self): @@ -168,6 +220,110 @@ def test_default(self): eng.create(ch, 'othergrp', 'someone', 'somewhere', 0) self.assertDictEqual(ch.perm, {'put':True, 'rpc':True, 'uncached':True, 'audit': False}) + def test_rule_unknown_predicate_fails_secure(self): + eng = DummyEngine(""" + ASG(DEFAULT) { + RULE(1,READ) + RULE(1,WRITE) { + FUTUREPRED(x) { foo(bar) } + } + } + """) + + ch = self.DummyChannel() + eng.create(ch, 'DEFAULT', 'someone', '1.2.3.4', 0) + self.assertDictEqual(ch.perm, {'put':False, 'rpc':False, 'uncached':False, 'audit': False}) + + def test_rule_method_authority_protocol(self): + eng = DummyEngine(""" + ASG(DEFAULT) { + RULE(1,READ) + RULE(1,WRITE) { + METHOD(x509) + AUTHORITY(ROOT) + PROTOCOL(TLS) + } + } + """) + + ch = self.DummyChannel() + eng.create(ch, 'DEFAULT', 'someone', '1.2.3.4', 0) + self.assertDictEqual(ch.perm, {'put':False, 'rpc':False, 'uncached':False, 'audit': False}) + + ch = self.DummyChannel() + eng.create(ch, 'DEFAULT', 'someone', '1.2.3.4', 0, + method='x509', authority='ROOT', protocol='TLS') + self.assertDictEqual(ch.perm, {'put':True, 'rpc':True, 'uncached':False, 'audit': False}) + + def test_ca_method_strips_prefixes(self): + eng = DummyEngine(""" + UAG(SPECIAL) { alice } + ASG(DEFAULT) { + RULE(1,WRITE) { UAG(SPECIAL) } + } + """) + + ch = self.DummyChannel() + eng.create(ch, 'DEFAULT', 'x509/alice', '1.2.3.4', 0, method='ca') + self.assertDictEqual(ch.perm, {'put':True, 'rpc':True, 'uncached':False, 'audit': False}) + + eng = DummyEngine(""" + ASG(DEFAULT) { + RULE(1,WRITE) { METHOD(x509) } + } + """) + + ch = self.DummyChannel() + eng.create(ch, 'DEFAULT', 'x509/alice', '1.2.3.4', 0, method='ca') + self.assertDictEqual(ch.perm, {'put':False, 'rpc':False, 'uncached':False, 'audit': False}) + + def test_authority_def_mapping(self): + eng = DummyEngine(r''' + AUTHORITY(EPICS, "EPICS Root Certificate Authority") + ASG(DEFAULT) { + RULE(1,WRITE) { + METHOD(x509) + PROTOCOL(TLS) + AUTHORITY(EPICS) + } + } + ''') + + ch = self.DummyChannel() + eng.create(ch, 'DEFAULT', 'operator', '1.2.3.4', 0, + method='x509', + protocol='TLS', + authority='EPICS Root Certificate Authority') + self.assertDictEqual(ch.perm, {'put':True, 'rpc':True, 'uncached':False, 'audit': False}) + + ch = self.DummyChannel() + eng.create(ch, 'DEFAULT', 'operator', '1.2.3.4', 0, + method='x509', + protocol='TLS', + authority='EPICS Root Certificate Authority\nSome Intermediate CA') + self.assertDictEqual(ch.perm, {'put':True, 'rpc':True, 'uncached':False, 'audit': False}) + + def test_authority_def_chained(self): + eng = DummyEngine(r''' + AUTHORITY("EPICS Root Certificate Authority") { + AUTHORITY(CMS_AUTH, "intermediateCA") + } + ASG(DEFAULT) { + RULE(1,WRITE) { + METHOD(x509) + PROTOCOL(TLS) + AUTHORITY(CMS_AUTH) + } + } + ''') + + ch = self.DummyChannel() + eng.create(ch, 'DEFAULT', 'operator', '1.2.3.4', 0, + method='x509', + protocol='TLS', + authority='EPICS Root Certificate Authority\nintermediateCA') + self.assertDictEqual(ch.perm, {'put':True, 'rpc':True, 'uncached':False, 'audit': False}) + def test_roles(self): eng = DummyEngine(""" UAG(SPECIAL) {