Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 64 additions & 0 deletions src/p4p.h
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@

#include <stdexcept>
#include <sstream>
#include <type_traits>
#include <utility>

#include <epicsMutex.h>
#include <epicsGuard.h>
Expand Down Expand Up @@ -50,6 +52,42 @@ namespace p4p {

using namespace pvxs;

namespace detail {

template<typename C>
static inline auto credAuthorityImpl(const C& c, int) -> decltype(std::declval<const C&>().authority, std::string())
{
return c.authority;
}

template<typename C>
static inline std::string credAuthorityImpl(const C& c, long)
{
try {
auto fld = c.raw["authority"];
if(fld.storageType()==StoreType::String)
return fld.template as<std::string>();
}catch(...){
}
return std::string();
}

template<typename C>
static inline auto credProtocolImpl(const C& c, int) -> decltype(std::declval<const C&>().isTLS, std::string())
{
return c.isTLS ? std::string("TLS") : std::string("TCP");
}

template<typename C>
static inline std::string credProtocolImpl(const C& c, long)
{
if(c.method=="x509")
return "TLS";
return "TCP";
}

}

typedef epicsGuard<epicsMutex> Guard;
typedef epicsGuardRelease<epicsMutex> UnGuard;

Expand Down Expand Up @@ -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);
Expand Down
39 changes: 38 additions & 1 deletion src/p4p/_gw.pyx
Original file line number Diff line number Diff line change
Expand Up @@ -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 "<p4p.h>" 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
Expand Down Expand Up @@ -79,7 +85,38 @@ cdef class InfoBase(object):
@property
def account(self):
if <bool>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 <bool>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 <bool>self.info:
return self.info.get().method.decode('UTF-8', 'replace')
else:
return u''

@property
def authority(self):
if <bool>self.info:
return credAuthority(self.info.get()[0]).decode('UTF-8', 'replace')
else:
return u''

@property
def protocol(self):
if <bool>self.info:
return credProtocol(self.info.get()[0]).decode('UTF-8', 'replace')
else:
return u''

Expand Down
4 changes: 3 additions & 1 deletion src/p4p/_p4p.pyx
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ cdef extern from "<p4p.h>" 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+
Expand Down Expand Up @@ -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}
Expand Down
99 changes: 88 additions & 11 deletions src/p4p/asLib/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import logging
import logging
import warnings
import socket
import re
Expand All @@ -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__)

Expand Down Expand Up @@ -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)
Expand All @@ -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':
Expand All @@ -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'])
Expand All @@ -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]]
Expand Down Expand Up @@ -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()

Expand All @@ -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:
Expand All @@ -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):
Expand All @@ -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())
Expand All @@ -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
Expand Down Expand Up @@ -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()
Expand Down
Loading