%% %% %CopyrightBegin% %% %% Copyright Ericsson AB 1997-2016. All Rights Reserved. %% %% Licensed under the Apache License, Version 2.0 (the "License"); %% you may not use this file except in compliance with the License. %% You may obtain a copy of the License at %% %% http://www.apache.org/licenses/LICENSE-2.0 %% %% Unless required by applicable law or agreed to in writing, software %% distributed under the License is distributed on an "AS IS" BASIS, %% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. %% See the License for the specific language governing permissions and %% limitations under the License. %% %% %CopyrightEnd% %% -module(snmpa_acm). -behaviour(snmpa_authentication_service). -export([init_check_access/2, get_root_mib_view/0, error2status/1, validate_mib_view/2, validate_all_mib_view/2, is_definitely_not_in_mib_view/2, invalidate_ca_cache/0]). -include("snmp_types.hrl"). -include("STANDARD-MIB.hrl"). -include("SNMP-FRAMEWORK-MIB.hrl"). -include("SNMPv2-TM.hrl"). -define(VMODULE,"ACM"). -include("snmp_verbosity.hrl"). %%%----------------------------------------------------------------- %%% This module implements the Access Control Model part of the %%% multi-lingual SNMP agent. It contains generic function not %%% tied to a specific model, but in this version it uses VACM. %%% %%% Note that we don't follow the isAccessAllowed Abstract Service %%% Interface defined in rfc2271. We implement an optimization %%% of that ASI. Since the mib view is the same for all variable %%% bindings in a PDU, there is no need to recalculate the mib %%% view for each variable. Therefore, one function %%% (init_check_access/2) is used to find the mib view, and then %%% each variable is checked against this mib view. %%% %%% Access checking is done in several steps. First, the version- %%% specific MPD (see snmpa_mpd) creates data used by VACM. This %%% means that the format of this data is known by both the MPD and %%% the ACM. When the master agent wants to check the access to a %%% Pdu, it first calls init_check_access/2, which returns a MibView %%% that can be used to check access of individual variables. %%%----------------------------------------------------------------- %%----------------------------------------------------------------- %% Func: init_check_access(Pdu, ACMData) -> %% {ok, MibView, ContextName} | %% {error, Reason} | %% {discarded, Variable, Reason} %% Types: Pdu = #pdu %% ACMData = acm_data() = %% {community, SecModel, Community, TDomain, TAddress} | %% {v3, MsgID, SecModel, SecName, SecLevel, %% ContextEngineID, ContextName, SecData} %% Community = string() %% TDomain = ?transportDomainUdpIpv4 | ?transportDomainUdpIpv6 %% TAddress = ip() ++ udp() (list) %% MsgID = integer() %% SecModel = ?SEC_* (see snmp_types.hrl) %% SecName = string() %% SecLevel = ?'SnmpSecurityLevel_*' (see SNMP-FRAMEWORK-MIB.hrl) %% ContextEngineID = string() %% ContextName = string() %% SecData = %% Variable = snmpInBadCommunityNames | %% snmpInBadCommunityUses | %% snmpInASNParseErrs %% Reason = snmp_message_decoding | %% {bad_community_name, Address, Community}} | %% {invalid_access, Access, Op} %% %% Purpose: Called once for each Pdu. Returns a MibView %% which is later used for each variable in the pdu. %% The authenticationFailure trap is sent (maybe) when the auth. %% procedure evaluates to unauthentic, %% %% NOTE: This function is executed in the Master agents's context %%----------------------------------------------------------------- init_check_access(Pdu, ACMData) -> case init_ca(Pdu, ACMData) of {ok, MibView, ContextName} -> {ok, MibView, ContextName}; {discarded, Reason} -> {error, Reason}; {authentication_failure, Variable, Reason} -> handle_authentication_failure(), {discarded, Variable, Reason} end. error2status(noSuchView) -> authorizationError; error2status(noAccessEntry) -> authorizationError; error2status(noGroupName) -> authorizationError; error2status(_) -> genErr. %%----------------------------------------------------------------- %% Func: init_ca(Pdu, ACMData) -> %% {ok, MibView} | %% {discarded, Reason} | %% {authentication_failure, Variable, Reason} %% %% error: an error response will be sent %% discarded: no error response is sent %% authentication_failure: no error response is sent, a trap is generated %%----------------------------------------------------------------- init_ca(Pdu, {community, SecModel, Community, TAddress}) -> TDomain = snmp_conf:mk_tdomain(snmp_target_mib:default_domain()), init_ca(Pdu, {community, SecModel, Community, TDomain, TAddress}); init_ca(Pdu, {community, SecModel, Community, TDomain, TAddress}) -> %% This is a v1 or v2c request. Use SNMP-COMMUNITY-MIB to %% map the community to vacm parameters. ?vtrace("check access for ~n" " Pdu: ~p~n" " Security model: ~p~n" " Community: ~s",[Pdu,SecModel,Community]), ViewType = case Pdu#pdu.type of 'set-request' -> write; _ -> read end, ?vtrace("View type: ~p", [ViewType]), CaCacheKey = {Community, SecModel, TDomain, TAddress, ViewType}, case check_ca_cache(CaCacheKey) of false -> case snmp_community_mib:community2vacm(Community, {TDomain, TAddress}) of {SecName, _ContextEngineId, ContextName} -> %% Maybe we should check that the contextEngineID %% matches the local engineID? %% It better, since we don't impl. proxy. ?vtrace("get mib view" "~n Security name: ~p" "~n Context name: ~p",[SecName, ContextName]), case snmpa_vacm:get_mib_view(ViewType, SecModel, SecName, ?'SnmpSecurityLevel_noAuthNoPriv', ContextName) of {ok, MibView} -> Res = {ok, MibView, ContextName}, upd_ca_cache({CaCacheKey, Res}), put(sec_model, SecModel), put(sec_name, SecName), Res; {discarded, Reason} -> snmpa_mpd:inc(snmpInBadCommunityUses), {discarded, Reason} end; undefined -> {authentication_failure, snmpInBadCommunityNames, {bad_community_name, TDomain, TAddress, Community}} end; Res -> Res end; init_ca(Pdu, {v3, _MsgID, SecModel, SecName, SecLevel, _ContextEngineID, ContextName, _SecData}) -> ?vtrace("check v3 access for ~n" " Pdu: ~p~n" " Security model: ~p~n" " Security name: ~p~n" " Security level: ~p",[Pdu,SecModel,SecName,SecLevel]), ViewType = case Pdu#pdu.type of 'set-request' -> write; _ -> read end, ?vtrace("View type: ~p",[ViewType]), %% Convert the msgflag value to a ?'SnmpSecurityLevel*' SL = case SecLevel of 0 -> ?'SnmpSecurityLevel_noAuthNoPriv'; 1 -> ?'SnmpSecurityLevel_authNoPriv'; 3 -> ?'SnmpSecurityLevel_authPriv' end, put(sec_model, SecModel), put(sec_name, SecName), CaCacheKey = {ViewType, SecModel, SecName, SL, ContextName}, case check_ca_cache(CaCacheKey) of false -> case snmpa_vacm:get_mib_view(ViewType, SecModel, SecName, SL, ContextName) of {ok, MibView} -> Res = {ok, MibView, ContextName}, upd_ca_cache({CaCacheKey, Res}), Res; Else -> Else end; Res -> Res end. check_ca_cache(Key) -> case get(ca_cache) of undefined -> put(ca_cache, []), false; L -> check_ca_cache(L, Key) end. check_ca_cache([{Key, Val} | _], Key) -> Val; check_ca_cache([_ | T], Key) -> check_ca_cache(T, Key); check_ca_cache([], _) -> false. upd_ca_cache(KeyVal) -> case get(ca_cache) of [A,B,C,_] -> % cache is full put(ca_cache, [KeyVal,A,B,C]); L -> put(ca_cache, [KeyVal|L]) end. invalidate_ca_cache() -> erase(ca_cache). %%----------------------------------------------------------------- %% Func: check(Res) -> {ok, MibView} | {discarded, Variable, Reason} %% Args: Res = {ok, AccessFunc} | %% {authentication_failure, Variable, Reason} %%----------------------------------------------------------------- %%----------------------------------------------------------------- %% NOTE: The do_get MUST be executed in the Master agents's %% context. Therefor, force master-agent to do a GET to %% retrieve the value for snmpEnableAuthenTraps. %% A user may have another impl. than default for this %% variable. %%----------------------------------------------------------------- handle_authentication_failure() -> case snmpa_agent:do_get(get_root_mib_view(), [#varbind{oid = ?snmpEnableAuthenTraps_instance}], true, true) of {noError, _, [#varbind{value = ?snmpEnableAuthenTraps_enabled}]} -> ?vtrace("handle_authentication_failure -> enabled", []), snmpa:send_notification(snmp_master_agent, authenticationFailure, no_receiver); _ -> ok end. %%%----------------------------------------------------------------- %%% MIB View handling %%%----------------------------------------------------------------- get_root_mib_view() -> [{[1], [], ?view_included}]. %%----------------------------------------------------------------- %% Returns true if Oid is in the MibView, false %% otherwise. %% Alg: (defined in SNMP-VIEW-BASED-ACM-MIB) %% For each family (= {SubTree, Mask, Type}), check if Oid %% belongs to that family. For each family that Oid belong to, %% get the longest. If two or more are longest, get the %% lexicografically greatest. Check the type of this family. If %% included, then Oid belongs to the MibView, otherwise it %% does not. %% Optimisation: Do only one loop, and kepp the largest sofar. %% When we find a family that Oid belongs to, check if it is %% larger than the largest. %%----------------------------------------------------------------- validate_mib_view(Oid, MibView) -> case get_largest_family(MibView, Oid, undefined) of {_, _, ?view_included} -> true; _ -> false end. get_largest_family([{SubTree, Mask, Type} | T], Oid, Res) -> case check_mask(Oid, SubTree, snmp_view_based_acm_mib:emask2imask(Mask)) of true -> get_largest_family(T, Oid, add_res(length(SubTree), SubTree, Type, Res)); false -> get_largest_family(T, Oid, Res) end; get_largest_family([], _Oid, Res) -> Res. %%----------------------------------------------------------------- %% We keep only the largest (first longest SubTree, and then %% lexicografically greatest) SubTree. %%----------------------------------------------------------------- add_res(Len, SubTree, Type, undefined) -> {Len, SubTree, Type}; add_res(Len, SubTree, Type, {MaxLen, _MaxS, _MaxT}) when Len > MaxLen -> {Len, SubTree, Type}; add_res(Len, SubTree, Type, {MaxLen, MaxS, MaxT}) when Len == MaxLen -> if SubTree > MaxS -> {Len, SubTree, Type}; true -> {MaxLen, MaxS, MaxT} end; add_res(_Len, _SubTree, _Type, MaxRes) -> MaxRes. %% 1 in mask is exact match, 0 is wildcard. %% If mask is shorter than SubTree, its regarded %% as being all ones. check_mask(_Oid, [], _Mask) -> true; check_mask([X | Xs], [X | Ys], [1 | Ms]) -> check_mask(Xs, Ys, Ms); check_mask([X | Xs], [X | Ys], []) -> check_mask(Xs, Ys, []); check_mask([_X | Xs], [_Y | Ys], [0 | Ms]) -> check_mask(Xs, Ys, Ms); check_mask(_, _, _) -> false. %%----------------------------------------------------------------- %% Validates all oids in the Varbinds list towards the MibView. %%----------------------------------------------------------------- validate_all_mib_view([#varbind{oid = Oid, org_index = Index} | Varbinds], MibView) -> ?vtrace("validate_all_mib_view -> entry with" "~n Oid: ~p" "~n Index: ~p", [Oid, Index]), case validate_mib_view(Oid, MibView) of true -> validate_all_mib_view(Varbinds, MibView); false -> {false, Index} end; validate_all_mib_view([], _MibView) -> ?vtrace("validate_all_mib_view -> done", []), true. %%----------------------------------------------------------------- %% This function is used to optimize the next operation in %% snmpa_mib_data. If we get to a node in the tree where we can %% determine that we are guaranteed to be outside the mibview, %% we don't have to continue the search in the that tree (Actually %% we will, because we only check this at leafs. But we won't %% go into tables or subagents, and that's the important %% optimization.) For now, this function isn't that sophisticated; %% it just checks that there is really no family in the mibview %% that the Oid (or any other oids with Oid as prefix) may be %% included in. Perhaps this function easily could be more %% intelligent. %%----------------------------------------------------------------- is_definitely_not_in_mib_view(Oid, [{SubTree, Mask,?view_included}|T]) -> case check_maybe_mask(Oid, SubTree, snmp_view_based_acm_mib:emask2imask(Mask)) of true -> false; false -> is_definitely_not_in_mib_view(Oid, T) end; is_definitely_not_in_mib_view(Oid, [{_SubTree, _Mask,?view_excluded}|T]) -> is_definitely_not_in_mib_view(Oid, T); is_definitely_not_in_mib_view(_Oid, []) -> true. %%----------------------------------------------------------------- %% As check_mask, BUT if Oid < SubTree and sofar good, we %% return true. As Oid get larger we may decide. %%----------------------------------------------------------------- check_maybe_mask(_Oid, [], _Mask) -> true; check_maybe_mask([X | Xs], [X | Ys], [1 | Ms]) -> check_maybe_mask(Xs, Ys, Ms); check_maybe_mask([X | Xs], [X | Ys], []) -> check_maybe_mask(Xs, Ys, []); check_maybe_mask([_X | Xs], [_Y | Ys], [0 | Ms]) -> check_maybe_mask(Xs, Ys, Ms); check_maybe_mask([_X | _Xs], [_Y | _Ys], _) -> false; check_maybe_mask(_, _, _) -> true.