aboutsummaryrefslogtreecommitdiffstats
path: root/lib/snmp/src/agent/snmpa_acm.erl
blob: 30bd34a205c7609ece28b797f0f71172cdf3bde2 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
%%
%% %CopyrightBegin%
%% 
%% Copyright Ericsson AB 1997-2009. All Rights Reserved.
%% 
%% The contents of this file are subject to the Erlang Public License,
%% Version 1.1, (the "License"); you may not use this file except in
%% compliance with the License. You should have received a copy of the
%% Erlang Public License along with this software. If not, it can be
%% retrieved online at http://www.erlang.org/.
%% 
%% Software distributed under the License is distributed on an "AS IS"
%% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See
%% the License for the specific language governing rights 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() <not used>
%%        SecModel        = ?SEC_*  (see snmp_types.hrl)
%%        SecName         = string()
%%        SecLevel        = ?'SnmpSecurityLevel_*' (see SNMP-FRAMEWORK-MIB.hrl)
%%        ContextEngineID = string() <not used>
%%        ContextName     = string()
%%        SecData         = <not used>
%%        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, 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, 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.