From 6c7b09ac88be195db176c37ca7a197265ca978d0 Mon Sep 17 00:00:00 2001 From: Ilya Etingof Date: Mon, 29 Jul 2019 09:57:45 +0200 Subject: Rework VACM access control function (#287) Most important changes include: * Added subtree match negation support (vacmViewTreeFamilyType) * Added subtree family mask support (vacmViewTreeFamilyMask) * Added prefix content name matching support (vacmAccessContextMatch) * Added key VACM tables caching for better lookup performance --- pysnmp/entity/config.py | 53 ++++-- pysnmp/proto/acmod/rfc3415.py | 400 +++++++++++++++++++++++++++++++++--------- 2 files changed, 353 insertions(+), 100 deletions(-) (limited to 'pysnmp') diff --git a/pysnmp/entity/config.py b/pysnmp/entity/config.py index 57e8704f..b484355a 100644 --- a/pysnmp/entity/config.py +++ b/pysnmp/entity/config.py @@ -19,6 +19,9 @@ from pysnmp.proto.secmod.rfc7860.auth import hmacsha2 from pysnmp.proto.secmod.eso.priv import aes192 from pysnmp.proto.secmod.eso.priv import aes256 from pysnmp.proto.secmod.eso.priv import des3 +from pysnmp.proto import rfc1902 +from pysnmp.proto import rfc1905 +from pysnmp import error # A shortcut to popular constants @@ -568,12 +571,11 @@ def __cookVacmAccessInfo(snmpEngine, groupName, contextName, securityModel, return vacmAccessEntry, tblIdx -def addVacmAccess(snmpEngine, groupName, contextName, securityModel, - securityLevel, prefix, readView, writeView, notifyView): +def addVacmAccess(snmpEngine, groupName, contextPrefix, securityModel, + securityLevel, contextMatch, readView, writeView, notifyView): vacmAccessEntry, tblIdx = __cookVacmAccessInfo( - snmpEngine, groupName, contextName, securityModel, securityLevel) - - addContext(snmpEngine, contextName) + snmpEngine, groupName, contextPrefix, securityModel, + securityLevel) snmpEngine.msgAndPduDsp.mibInstrumController.writeMibObjects( (vacmAccessEntry.name + (9,) + tblIdx, 'destroy'), @@ -581,10 +583,10 @@ def addVacmAccess(snmpEngine, groupName, contextName, securityModel, ) snmpEngine.msgAndPduDsp.mibInstrumController.writeMibObjects( - (vacmAccessEntry.name + (1,) + tblIdx, contextName), + (vacmAccessEntry.name + (1,) + tblIdx, contextPrefix), (vacmAccessEntry.name + (2,) + tblIdx, securityModel), (vacmAccessEntry.name + (3,) + tblIdx, securityLevel), - (vacmAccessEntry.name + (4,) + tblIdx, prefix), + (vacmAccessEntry.name + (4,) + tblIdx, contextMatch), (vacmAccessEntry.name + (5,) + tblIdx, readView), (vacmAccessEntry.name + (6,) + tblIdx, writeView), (vacmAccessEntry.name + (7,) + tblIdx, notifyView), @@ -593,13 +595,11 @@ def addVacmAccess(snmpEngine, groupName, contextName, securityModel, ) -def delVacmAccess(snmpEngine, groupName, contextName, securityModel, +def delVacmAccess(snmpEngine, groupName, contextPrefix, securityModel, securityLevel): vacmAccessEntry, tblIdx = __cookVacmAccessInfo( - snmpEngine, groupName, contextName, securityModel, securityLevel) - - delContext(snmpEngine, contextName) + snmpEngine, groupName, contextPrefix, securityModel, securityLevel) snmpEngine.msgAndPduDsp.mibInstrumController.writeMibObjects( (vacmAccessEntry.name + (9,) + tblIdx, 'destroy'), @@ -618,7 +618,19 @@ def __cookVacmViewInfo(snmpEngine, viewName, subTree): return vacmViewTreeFamilyEntry, tblIdx -def addVacmView(snmpEngine, viewName, viewType, subTree, mask): +def addVacmView(snmpEngine, viewName, viewType, subTree, subTreeMask): + + # Allow bitmask specification in form of an OID + if '.' in subTreeMask: + subTreeMask = rfc1902.ObjectIdentifier(subTreeMask) + + if isinstance(subTreeMask, rfc1902.ObjectIdentifier): + subTreeMask = tuple(subTreeMask) + if len(subTreeMask) < len(subTree): + subTreeMask += (1,) * (len(subTree) - len(subTreeMask)) + + subTreeMask = rfc1902.OctetString.fromBinaryString( + ''.join(str(x) for x in subTreeMask)) vacmViewTreeFamilyEntry, tblIdx = __cookVacmViewInfo( snmpEngine, viewName, subTree) @@ -631,7 +643,7 @@ def addVacmView(snmpEngine, viewName, viewType, subTree, mask): snmpEngine.msgAndPduDsp.mibInstrumController.writeMibObjects( (vacmViewTreeFamilyEntry.name + (1,) + tblIdx, viewName), (vacmViewTreeFamilyEntry.name + (2,) + tblIdx, subTree), - (vacmViewTreeFamilyEntry.name + (3,) + tblIdx, mask), + (vacmViewTreeFamilyEntry.name + (3,) + tblIdx, subTreeMask), (vacmViewTreeFamilyEntry.name + (4,) + tblIdx, viewType), (vacmViewTreeFamilyEntry.name + (6,) + tblIdx, 'createAndGo'), snmpEngine=snmpEngine @@ -671,19 +683,21 @@ def addVacmUser(snmpEngine, securityModel, securityName, securityLevel, notifyView) = __cookVacmUserInfo( snmpEngine, securityModel, securityName, securityLevel) + addContext(snmpEngine, contextName) + addVacmGroup(snmpEngine, groupName, securityModel, securityName) addVacmAccess(snmpEngine, groupName, contextName, securityModel, - securityLevel, 1, readView, writeView, notifyView) + securityLevel, 'exact', readView, writeView, notifyView) if readSubTree: - addVacmView(snmpEngine, readView, "included", readSubTree, null) + addVacmView(snmpEngine, readView, 'included', readSubTree, null) if writeSubTree: - addVacmView(snmpEngine, writeView, "included", writeSubTree, null) + addVacmView(snmpEngine, writeView, 'included', writeSubTree, null) if notifySubTree: - addVacmView(snmpEngine, notifyView, "included", notifySubTree, null) + addVacmView(snmpEngine, notifyView, 'included', notifySubTree, null) def delVacmUser(snmpEngine, securityModel, securityName, securityLevel, @@ -693,9 +707,10 @@ def delVacmUser(snmpEngine, securityModel, securityName, securityLevel, notifyView) = __cookVacmUserInfo( snmpEngine, securityModel, securityName, securityLevel) + delContext(snmpEngine, contextName) delVacmGroup(snmpEngine, securityModel, securityName) - - delVacmAccess(snmpEngine, groupName, contextName, securityModel, securityLevel) + delVacmAccess(snmpEngine, groupName, contextName, + securityModel, securityLevel) if readSubTree: delVacmView(snmpEngine, readView, readSubTree) diff --git a/pysnmp/proto/acmod/rfc3415.py b/pysnmp/proto/acmod/rfc3415.py index 13d9de12..4d987666 100644 --- a/pysnmp/proto/acmod/rfc3415.py +++ b/pysnmp/proto/acmod/rfc3415.py @@ -17,127 +17,365 @@ class Vacm(object): _powOfTwoSeq = (128, 64, 32, 16, 8, 4, 2, 1) - def isAccessAllowed(self, snmpEngine, securityModel, securityName, - securityLevel, viewType, contextName, variableName): + def __init__(self): + self._contextBranchId = -1 + self._groupNameBranchId = -1 + self._accessBranchId = -1 + self._viewTreeBranchId = -1 + + self._contextMap = {} + self._groupNameMap = {} + self._accessMap = {} + self._viewTreeMap = {} + + def _addAccessEntry(self, groupName, contextPrefix, securityModel, + securityLevel, prefixMatch, readView, writeView, + notifyView): + if not groupName: + return + + groups = self._accessMap + + try: + views = groups[groupName] + + except KeyError: + views = groups[groupName] = {} + + for viewType, viewName in ( + ('read', readView), ('write', writeView), + ('notify', notifyView)): + + try: + matches = views[viewType] + + except KeyError: + matches = views[viewType] = {} + + try: + contexts = matches[prefixMatch] + + except KeyError: + contexts = matches[prefixMatch] = {} + + try: + models = contexts[contextPrefix] + + except KeyError: + models = contexts[contextPrefix] = {} + + try: + levels = models[securityModel] + + except KeyError: + levels = models[securityModel] = {} + + levels[securityLevel] = viewName + + def _getFamilyViewName(self, groupName, contextName, securityModel, securityLevel, viewType): + groups = self._accessMap + + try: + views = groups[groupName] + + except KeyError: + raise error.StatusInformation(errorIndication=errind.noGroupName) + + try: + matches = views[viewType] + + except KeyError: + raise error.StatusInformation(errorIndication=errind.noAccessEntry) + + try: + # vacmAccessTable #2: exact match shortcut + return matches[1][contextName][securityModel][securityLevel] + + except KeyError: + pass + + # vacmAccessTable #2: fuzzy look-up + + candidates = [] + + for match, names in matches.items(): + + for context, models in names.items(): + + if match == 1 and contextName != context: + continue + + if match == 2 and contextName[:len(context)] != context: + continue + + for model, levels in models.items(): + for level, viewName in levels.items(): + + # priorities: + # - matching securityModel + # - exact context name match + # - longer partial match + # - highest securityLevel + rating = securityModel == model, match == 1, len(context), level + + candidates.append((rating, viewName)) + + if not candidates: + raise error.StatusInformation(errorIndication=errind.notInView) + + candidates.sort() + + rating, viewName = candidates[0] + return viewName + + def isAccessAllowed(self, + snmpEngine, + securityModel, + securityName, + securityLevel, + viewType, + contextName, + variableName): mibInstrumController = snmpEngine.msgAndPduDsp.mibInstrumController mibBuilder = mibInstrumController.mibBuilder - debug.logger & debug.FLAG_ACL and debug.logger( + debug.logger & debug.flagACL and debug.logger( 'isAccessAllowed: securityModel %s, securityName %s, ' 'securityLevel %s, viewType %s, contextName %s for ' - 'variableName %s' % (securityModel, securityName, securityLevel, - viewType, contextName, variableName)) + 'variableName %s' % (securityModel, securityName, + securityLevel, viewType, contextName, + variableName)) - # 3.2.1 - vacmContextEntry, = mibBuilder.importSymbols( - 'SNMP-VIEW-BASED-ACM-MIB', 'vacmContextEntry') + # Rebuild contextName map if changed - tblIdx = vacmContextEntry.getInstIdFromIndices(contextName) + vacmContextName, = mibInstrumController.mibBuilder.importSymbols( + 'SNMP-VIEW-BASED-ACM-MIB', 'vacmContextName') - try: - vacmContextEntry.getNode( - vacmContextEntry.name + (1,) + tblIdx).syntax + if self._contextBranchId != vacmContextName.branchVersionId: + + self._contextMap.clear() + + nextMibNode = vacmContextName + + while True: + try: + nextMibNode = vacmContextName.getNextNode(nextMibNode.name) - except NoSuchInstanceError: + except NoSuchInstanceError: + break + + self._contextMap[nextMibNode.syntax] = True + + self._contextBranchId = vacmContextName.branchVersionId + + # 3.2.1 + if contextName not in self._contextMap: raise error.StatusInformation(errorIndication=errind.noSuchContext) - # 3.2.2 - vacmSecurityToGroupEntry, = mibBuilder.importSymbols( - 'SNMP-VIEW-BASED-ACM-MIB', 'vacmSecurityToGroupEntry') + # Rebuild groupName map if changed + + vacmGroupName, = mibInstrumController.mibBuilder.importSymbols( + 'SNMP-VIEW-BASED-ACM-MIB', 'vacmGroupName') + + if self._groupNameBranchId != vacmGroupName.branchVersionId: + + vacmSecurityToGroupEntry, = mibInstrumController.mibBuilder.importSymbols( + 'SNMP-VIEW-BASED-ACM-MIB', 'vacmSecurityToGroupEntry') + + self._groupNameMap.clear() + + nextMibNode = vacmGroupName + + while True: + try: + nextMibNode = vacmGroupName.getNextNode(nextMibNode.name) + + except NoSuchInstanceError: + break - tblIdx = vacmSecurityToGroupEntry.getInstIdFromIndices( - securityModel, securityName) + instId = nextMibNode.name[len(vacmGroupName.name):] + + indices = vacmSecurityToGroupEntry.getIndicesFromInstId(instId) + + self._groupNameMap[indices] = nextMibNode.syntax + + self._groupNameBranchId = vacmGroupName.branchVersionId + + # 3.2.2 + indices = securityModel, securityName try: - vacmGroupName = vacmSecurityToGroupEntry.getNode( - vacmSecurityToGroupEntry.name + (3,) + tblIdx).syntax + groupName = self._groupNameMap[indices] - except NoSuchInstanceError: + except KeyError: raise error.StatusInformation(errorIndication=errind.noGroupName) - # 3.2.3 - vacmAccessEntry, = mibBuilder.importSymbols( - 'SNMP-VIEW-BASED-ACM-MIB', 'vacmAccessEntry') + # Rebuild access map if changed - # XXX partial context name match - tblIdx = vacmAccessEntry.getInstIdFromIndices( - vacmGroupName, contextName, securityModel, securityLevel) + vacmAccessStatus, = mibInstrumController.mibBuilder.importSymbols( + 'SNMP-VIEW-BASED-ACM-MIB', 'vacmAccessStatus') - # 3.2.4 - if viewType == 'read': - entryIdx = vacmAccessEntry.name + (5,) + tblIdx + if self._accessBranchId != vacmAccessStatus.branchVersionId: - elif viewType == 'write': - entryIdx = vacmAccessEntry.name + (6,) + tblIdx + (vacmAccessEntry, + vacmAccessContextPrefix, + vacmAccessSecurityModel, + vacmAccessSecurityLevel, + vacmAccessContextMatch, + vacmAccessReadViewName, + vacmAccessWriteViewName, + vacmAccessNotifyViewName) = mibInstrumController.mibBuilder.importSymbols( + 'SNMP-VIEW-BASED-ACM-MIB', + 'vacmAccessEntry', + 'vacmAccessContextPrefix', + 'vacmAccessSecurityModel', + 'vacmAccessSecurityLevel', + 'vacmAccessContextMatch', + 'vacmAccessReadViewName', + 'vacmAccessWriteViewName', + 'vacmAccessNotifyViewName') - elif viewType == 'notify': - entryIdx = vacmAccessEntry.name + (7,) + tblIdx + self._accessMap.clear() - else: - raise error.ProtocolError('Unknown view type %s' % viewType) + nextMibNode = vacmAccessStatus - try: - viewName = vacmAccessEntry.getNode(entryIdx).syntax + while True: + try: + nextMibNode = vacmAccessStatus.getNextNode(nextMibNode.name) - except NoSuchInstanceError: - raise error.StatusInformation(errorIndication=errind.noAccessEntry) + except NoSuchInstanceError: + break - if not viewName: - raise error.StatusInformation(errorIndication=errind.noSuchView) + if nextMibNode.syntax != 1: # active row + continue - # XXX split onto object & instance ? + instId = nextMibNode.name[len(vacmAccessStatus.name):] - # 3.2.5a - vacmViewTreeFamilyEntry, = mibInstrumController.mibBuilder.importSymbols( - 'SNMP-VIEW-BASED-ACM-MIB', 'vacmViewTreeFamilyEntry') + indices = vacmAccessEntry.getIndicesFromInstId(instId) + + vacmGroupName = indices[0] + + self._addAccessEntry( + vacmGroupName, + vacmAccessContextPrefix.getNode( + vacmAccessContextPrefix.name + instId).syntax, + vacmAccessSecurityModel.getNode( + vacmAccessSecurityModel.name + instId).syntax, + vacmAccessSecurityLevel.getNode( + vacmAccessSecurityLevel.name + instId).syntax, + vacmAccessContextMatch.getNode( + vacmAccessContextMatch.name + instId).syntax, + vacmAccessReadViewName.getNode( + vacmAccessReadViewName.name + instId).syntax, + vacmAccessWriteViewName.getNode( + vacmAccessWriteViewName.name + instId).syntax, + vacmAccessNotifyViewName.getNode( + vacmAccessNotifyViewName.name + instId).syntax + ) + + self._accessBranchId = vacmAccessStatus.branchVersionId - tblIdx = vacmViewTreeFamilyEntry.getInstIdFromIndices(viewName) + viewName = self._getFamilyViewName( + groupName, contextName, securityModel, securityLevel, viewType) - # Walk over entries - initialTreeName = treeName = vacmViewTreeFamilyEntry.name + (2,) + tblIdx + # Rebuild family subtree map if changed - maskName = vacmViewTreeFamilyEntry.name + (3,) + tblIdx + vacmViewTreeFamilyViewName, = mibInstrumController.mibBuilder.importSymbols( + 'SNMP-VIEW-BASED-ACM-MIB', 'vacmViewTreeFamilyViewName') - while True: - vacmViewTreeFamilySubtree = vacmViewTreeFamilyEntry.getNextNode( - treeName) + if self._viewTreeBranchId != vacmViewTreeFamilyViewName.branchVersionId: - vacmViewTreeFamilyMask = vacmViewTreeFamilyEntry.getNextNode( - maskName) + (vacmViewTreeFamilySubtree, + vacmViewTreeFamilyMask, + vacmViewTreeFamilyType) = mibInstrumController.mibBuilder.importSymbols( + 'SNMP-VIEW-BASED-ACM-MIB', + 'vacmViewTreeFamilySubtree', + 'vacmViewTreeFamilyMask', + 'vacmViewTreeFamilyType') - treeName = vacmViewTreeFamilySubtree.name - maskName = vacmViewTreeFamilyMask.name + self._viewTreeMap.clear() + + powerOfTwo = [2 ** exp for exp in range(7, -1, -1)] + + nextMibNode = vacmViewTreeFamilyViewName + + while True: + try: + nextMibNode = vacmViewTreeFamilyViewName.getNextNode( + nextMibNode.name) + + except NoSuchInstanceError: + break + + if nextMibNode.syntax not in self._viewTreeMap: + self._viewTreeMap[nextMibNode.syntax] = [] + + instId = nextMibNode.name[len(vacmViewTreeFamilyViewName.name):] + + subtree = vacmViewTreeFamilySubtree.getNode( + vacmViewTreeFamilySubtree.name + instId).syntax + + mask = vacmViewTreeFamilyMask.getNode( + vacmViewTreeFamilyMask.name + instId).syntax + + mode = vacmViewTreeFamilyType.getNode( + vacmViewTreeFamilyType.name + instId).syntax + + mask = mask.asNumbers() + maskLength = min(len(mask) * 8, len(subtree)) + + ignoredSubOids = [ + i * 8 + j for i, octet in enumerate(mask) + for j, bit in enumerate(powerOfTwo) + if not (bit & octet) and i * 8 + j < maskLength + ] + + if ignoredSubOids: + pattern = list(subtree) + + for ignoredSubOid in ignoredSubOids: + pattern[ignoredSubOid] = 0 + + subtree = subtree.clone(pattern) + + entry = subtree, ignoredSubOids, mode == 1 + + self._viewTreeMap[nextMibNode.syntax].append(entry) + + for entries in self._viewTreeMap.values(): + entries.sort(key=lambda x: (len(x[0]), x[0])) + + self._viewTreeBranchId = vacmViewTreeFamilyViewName.branchVersionId + + # 3.2.5a + indices = viewName + + try: + entries = self._viewTreeMap[indices] - if initialTreeName != treeName[:len(initialTreeName)]: - # 3.2.5b - raise error.StatusInformation(errorIndication=errind.notInView) + except KeyError: + return error.StatusInformation(errorIndication=errind.notInView) - l = len(vacmViewTreeFamilySubtree.syntax) - if l > len(variableName): - continue + accessAllowed = False - if vacmViewTreeFamilyMask.syntax: - mask = [] - for c in vacmViewTreeFamilyMask.syntax.asNumbers(): - mask.extend([b & c for b in self._powOfTwoSeq]) + for entry in entries: + subtree, ignoredSubOids, included = entry - m = len(mask) - 1 - idx = l - 1 + if ignoredSubOids: + subOids = list(variableName) - while idx: - if (idx > m or mask[idx] and - vacmViewTreeFamilySubtree.syntax[idx] != variableName[idx]): - break + for ignoredSubOid in ignoredSubOids: + subOids[ignoredSubOid] = 0 - idx -= 1 + normalizedVariableName = subtree.clone(subOids) - if idx: - continue # no match + else: + normalizedVariableName = variableName - else: # no mask - if vacmViewTreeFamilySubtree.syntax != variableName[:l]: - continue # no match + if subtree.isPrefixOf(normalizedVariableName): + accessAllowed = included - # 3.2.5c - return error.StatusInformation(errorIndication=errind.accessAllowed) + # 3.2.5c + if not accessAllowed: + raise error.StatusInformation(errorIndication=errind.notInView) -- cgit v1.2.1