1%%
2%% %CopyrightBegin%
3%%
4%% Copyright Ericsson AB 1999-2015. All Rights Reserved.
5%%
6%% Licensed under the Apache License, Version 2.0 (the "License");
7%% you may not use this file except in compliance with the License.
8%% You may obtain a copy of the License at
9%%
10%%     http://www.apache.org/licenses/LICENSE-2.0
11%%
12%% Unless required by applicable law or agreed to in writing, software
13%% distributed under the License is distributed on an "AS IS" BASIS,
14%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15%% See the License for the specific language governing permissions and
16%% limitations under the License.
17%%
18%% %CopyrightEnd%
19%%
20-module(snmpa_vacm).
21
22-export([get_mib_view/5]).
23-export([init/1, init/2, backup/1]).
24-export([delete/1, get_row/1, get_next_row/1, insert/1, insert/2,
25	 cleanup/0, dump_table/0]).
26
27-include("SNMPv2-TC.hrl").
28-include("SNMP-VIEW-BASED-ACM-MIB.hrl").
29-include("SNMP-FRAMEWORK-MIB.hrl").
30-include("snmp_types.hrl").
31-include("snmpa_vacm.hrl").
32
33-define(VMODULE,"VACM").
34-include("snmp_verbosity.hrl").
35
36
37%%%-----------------------------------------------------------------
38%%% Access Control Module for VACM  (see also snmpa_acm)
39%%% This module implements:
40%%%   1. access control functions for VACM
41%%%   2. vacmAccessTable as an ordered ets table
42%%%
43%%% This version of VACM handles v1, v2c and v3.
44%%%-----------------------------------------------------------------
45
46%%%-----------------------------------------------------------------
47%%%   1.  access control functions for VACM
48%%%-----------------------------------------------------------------
49%%-----------------------------------------------------------------
50%% Func: get_mib_view/5 -> {ok, ViewName} |
51%%                         {discarded, Reason}
52%% Types: ViewType    = read | write | notify
53%%        SecModel    = ?SEC_*  (see snmp_types.hrl)
54%%        SecName     = string()
55%%        SecLevel    = ?'SnmpSecurityLevel_*' (see SNMP-FRAMEWORK-MIB.hrl)
56%%        ContextName = string()
57%% Purpose: This function is used to map VACM parameters to a mib
58%%          view.
59%%-----------------------------------------------------------------
60get_mib_view(ViewType, SecModel, SecName, SecLevel, ContextName) ->
61    check_auth(catch auth(ViewType, SecModel, SecName, SecLevel, ContextName)).
62
63
64%% Follows the procedure in rfc2275
65auth(ViewType, SecModel, SecName, SecLevel, ContextName) ->
66    ?vtrace("auth -> entry with"
67	    "~n   ViewType:    ~p"
68	    "~n   SecModel:    ~p"
69	    "~n   SecName:     ~p"
70	    "~n   SecLevel:    ~p"
71	    "~n   ContextName: ~p",
72	    [ViewType, SecModel, SecName, SecLevel, ContextName]),
73    % 3.2.1 - Check that the context is known to us
74    ?vdebug("check that the context (~p) is known to us",[ContextName]),
75    case snmp_view_based_acm_mib:vacmContextTable(get, ContextName,
76						  [?vacmContextName]) of
77	[_Found] ->
78	    ok;
79	_ ->
80	    snmpa_mpd:inc(snmpUnknownContexts),
81	    throw({discarded, noSuchContext})
82    end,
83    % 3.2.2 - Check that the SecModel and SecName is valid
84    ?vdebug("check that SecModel (~p) and SecName (~p) is valid",
85	    [SecModel, SecName]),
86    GroupName =
87	case snmp_view_based_acm_mib:get(vacmSecurityToGroupTable,
88					 [SecModel, length(SecName) | SecName],
89		 [?vacmGroupName, ?vacmSecurityToGroupStatus]) of
90	    [{value, GN}, {value, ?'RowStatus_active'}] ->
91		GN;
92	    [{value, _GN}, {value, RowStatus}] ->
93		?vlog("valid SecModel and SecName but wrong row status:"
94		      "~n   RowStatus: ~p", [RowStatus]),
95		throw({discarded, noGroupName});
96	    _ ->
97		throw({discarded, noGroupName})
98	end,
99    % 3.2.3-4 - Find an access entry and its view name
100    ?vdebug("find an access entry and its view name",[]),
101    ViewName =
102	case get_view_name(ViewType, GroupName, ContextName,
103			   SecModel, SecLevel) of
104	    {ok, VN} -> VN;
105	    Error -> throw(Error)
106	end,
107    % 3.2.5a - Find the corresponding mib view
108    ?vdebug("find the corresponding mib view (for ~p)",[ViewName]),
109    get_mib_view(ViewName).
110
111check_auth({'EXIT',    Error})  -> exit(Error);
112check_auth({discarded, Reason}) -> {discarded, Reason};
113check_auth(Res)                 -> {ok, Res}.
114
115%%-----------------------------------------------------------------
116%% Returns a list of {ViewSubtree, ViewMask, ViewType}
117%% The view table is index by ViewIndex, ViewSubtree,
118%% so a next on ViewIndex returns the first
119%% key in the table >= ViewIndex.
120%%-----------------------------------------------------------------
121get_mib_view(ViewName) ->
122    ?vtrace("get_mib_view -> entry with"
123	    "~n   ViewName: ~p", [ViewName]),
124    ViewKey = [length(ViewName) | ViewName],
125    case snmp_view_based_acm_mib:table_next(vacmViewTreeFamilyTable,
126					    ViewKey) of
127	endOfTable ->
128	    {discarded, noSuchView};
129	Indexes ->
130	    case split_prefix(ViewKey, Indexes) of
131		{ok, Subtree} ->
132		    loop_mib_view(ViewKey, Subtree, Indexes, []);
133		false ->
134		    {discarded, noSuchView}
135	    end
136    end.
137
138split_prefix([H|T], [H|T2]) -> split_prefix(T,T2);
139split_prefix([], Rest) -> {ok, Rest};
140split_prefix(_, _) -> false.
141
142
143%% ViewName is including length from now on
144loop_mib_view(ViewName, Subtree, Indexes, MibView) ->
145    [{value, Mask}, {value, Type}, {value, Status}] =
146	snmp_view_based_acm_mib:vacmViewTreeFamilyTable(
147	  get, Indexes,
148	  [?vacmViewTreeFamilyMask,
149	   ?vacmViewTreeFamilyType,
150	   ?vacmViewTreeFamilyStatus]),
151    NextMibView =
152	case Status of
153	    ?'RowStatus_active' ->
154		[_Length | Tree] = Subtree,
155		[{Tree, Mask, Type} | MibView];
156	    _ ->
157		MibView
158	end,
159    case snmp_view_based_acm_mib:table_next(vacmViewTreeFamilyTable,
160					    Indexes) of
161	endOfTable -> NextMibView;
162	NextIndexes ->
163	    case split_prefix(ViewName, NextIndexes) of
164		{ok, NextSubTree} ->
165		    loop_mib_view(ViewName, NextSubTree, NextIndexes,
166				  NextMibView);
167		false ->
168		    NextMibView
169	    end
170    end.
171
172%%%-----------------------------------------------------------------
173%%%  1b.  The ordered ets table that implements vacmAccessTable
174%%%-----------------------------------------------------------------
175
176init(Dir) ->
177    init(Dir, terminate).
178
179init(Dir, InitError) ->
180    FName = filename:join(Dir, "snmpa_vacm.db"),
181    case file:read_file_info(FName) of
182	{ok, _} ->
183	    %% File exists - we must check this, since ets doesn't tell
184	    %% us the reason in case of error...
185	    case ets:file2tab(FName) of
186		{ok, _Tab} ->
187		    gc_tab([]);
188		{error, Reason} ->
189		    user_err("Corrupt VACM database ~p", [FName]),
190		    case InitError of
191			terminate ->
192			    throw({error, {file2tab, FName, Reason}});
193			_ ->
194			    %% Rename old file (for later analyzes)
195			    Saved = FName ++ ".saved",
196			    file:rename(FName, Saved),
197			    ets:new(snmpa_vacm,
198				    [public, ordered_set, named_table])
199		    end
200	    end;
201	{error, _} ->
202	    ets:new(snmpa_vacm, [public, ordered_set, named_table])
203    end,
204    ets:insert(snmp_agent_table, {snmpa_vacm_file, FName}),
205    {ok, FName}.
206
207
208backup(BackupDir) ->
209    BackupFile = filename:join(BackupDir, "snmpa_vacm.db"),
210    ets:tab2file(snmpa_vacm, BackupFile).
211
212
213%% Ret: {ok, ViewName} | {error, Reason}
214get_view_name(ViewType, GroupName, ContextName, SecModel, SecLevel) ->
215    ?vtrace("get_view_name -> entry with"
216	    "~n   ViewType:    ~p"
217	    "~n   GroupName:   ~p"
218	    "~n   ContextName: ~p"
219	    "~n   SecModel:    ~p"
220	    "~n   SecLevel:    ~p",
221	    [ViewType, GroupName, ContextName, SecModel, SecLevel]),
222    GroupKey = [length(GroupName) | GroupName],
223    case get_access_row(GroupKey, ContextName, SecModel, SecLevel) of
224	undefined ->
225	    {discarded, noAccessEntry};
226	Row ->
227	    ?vtrace("get_view_name -> Row: ~n   ~p", [Row]),
228	    ViewName =
229		case ViewType of
230		    read -> element(?vacmAReadViewName, Row);
231		    write -> element(?vacmAWriteViewName, Row);
232		    notify -> element(?vacmANotifyViewName, Row)
233		end,
234	    case ViewName of
235		"" ->
236		    ?vtrace("get_view_name -> not found when"
237			    "~n   ViewType:    ~p"
238			    "~n   GroupName:   ~p"
239			    "~n   ContextName: ~p"
240			    "~n   SecModel:    ~p"
241			    "~n   SecLevel:    ~p", [ViewType, GroupName,
242						     ContextName, SecModel,
243						     SecLevel]),
244		    {discarded, noSuchView};
245		_ -> {ok, ViewName}
246	    end
247    end.
248
249
250get_row(Key) ->
251    case ets:lookup(snmpa_vacm, Key) of
252	[{_Key, Row}] -> {ok, Row};
253	_ -> false
254    end.
255
256get_next_row(Key) ->
257    case ets:next(snmpa_vacm, Key) of
258	'$end_of_table' -> false;
259	NextKey  ->
260	    case ets:lookup(snmpa_vacm, NextKey) of
261		[Entry] -> Entry;
262		_ -> false
263	    end
264    end.
265
266insert(Entries) -> insert(Entries, true).
267
268insert(Entries, Dump) ->
269    lists:foreach(fun(Entry) -> ets:insert(snmpa_vacm, Entry) end, Entries),
270    dump_table(Dump).
271
272delete(Key) ->
273    ets:delete(snmpa_vacm, Key),
274    dump_table().
275
276
277cleanup() ->
278    ets:delete_all_objects(snmpa_vacm),
279    dump_table().
280
281dump_table(true) ->
282    dump_table();
283dump_table(_) ->
284    ok.
285
286
287dump_table() ->
288    [{_, FName}] = ets:lookup(snmp_agent_table, snmpa_vacm_file),
289    TmpName = unique_table_name(FName),
290    case ets:tab2file(snmpa_vacm, TmpName) of
291	ok ->
292	    case file:rename(TmpName, FName) of
293		ok ->
294		    ok;
295		Else -> % What is this? Undocumented return code...
296		    user_err("Warning: could not move VACM db ~p"
297			     " (~p)", [FName, Else])
298	    end;
299	{error, Reason} ->
300	    user_err("Warning: could not save vacm db ~p (~p)",
301		     [FName, Reason])
302    end.
303
304%% This little thing is an attempt to create a "unique" filename
305%% in order to minimize the risk of two processes at the same
306%% time dumping the table.
307unique_table_name(Pre) ->
308    %% We want something that is guaranteed to be unique,
309    %% therefor we use erlang:timestamp() instead of os:timestamp()
310    unique_table_name(Pre, erlang:timestamp()).
311
312unique_table_name(Pre, {_A, _B, C} = Now) ->
313    {Date, Time}     = calendar:now_to_datetime(Now),
314    {YYYY, MM, DD}   = Date,
315    {Hour, Min, Sec} = Time,
316    FormatDate =
317        io_lib:format("~.4w~.2.0w~.2.0w_~.2.0w~.2.0w~.2.0w_~w",
318                      [YYYY, MM, DD, Hour, Min, Sec, round(C/1000)]),
319    unique_table_name2(Pre, FormatDate).
320
321unique_table_name2(Pre, FormatedDate) ->
322    PidPart = unique_table_name_pid(),
323    lists:flatten(io_lib:format("~s.~s~s.tmp", [Pre, PidPart, FormatedDate])).
324
325unique_table_name_pid() ->
326    case string:tokens(pid_to_list(self()), [$<,$.,$>]) of
327	[A, B, C] ->
328	    A ++ B ++ C ++ ".";
329	_ ->
330	    ""
331    end.
332
333
334%%-----------------------------------------------------------------
335%% Alg.
336%% Procedure is defined in the descr. of vacmAccessTable.
337%%
338%% for (each entry with matching group name, context, secmodel and seclevel)
339%% {
340%%   rate the entry; if it's score is > prev max score, keep it
341%% }
342%%
343%% Rating:  The procedure says to keep entries in order
344%%    1.  matching secmodel  ('any'(0) or same(1) is ok)
345%%    2.  matching contextprefix (exact(1) or prefix(0) is ok)
346%%    3.  longest prefix (0..32)
347%%    4.  highest secLevel (noAuthNoPriv(0) < authNoPriv(1) < authPriv(2))
348%%  We give each entry a single rating number according to this order.
349%%  The number is chosen so that a higher number gives a better
350%%  entry, according to the order above.
351%%  The number is:
352%%    secLevel + (3 * prefix_len) + (99 * match_prefix) + (198 * match_secmodel)
353%%
354%% Optimisation:  Maybe the most common case is that there
355%% is just one matching entry, and it matches exact.  We could do
356%% an exact lookup for this entry; if we find one, use it, otherwise
357%% perform this alg.
358%%-----------------------------------------------------------------
359get_access_row(GroupKey, ContextName, SecModel, SecLevel) ->
360    %% First, try the optimisation...
361    ExactKey =
362	GroupKey ++ [length(ContextName) | ContextName] ++ [SecModel,SecLevel],
363    case ets:lookup(snmpa_vacm, ExactKey) of
364	[{_Key, Row}] ->
365	    Row;
366	_ -> % Otherwise, perform the alg
367	    get_access_row(GroupKey, GroupKey, ContextName,
368			   SecModel, SecLevel, 0, undefined)
369    end.
370
371get_access_row(Key, GroupKey, ContextName, SecModel, SecLevel, Score, Found) ->
372    case get_next_row(Key) of
373	{NextKey, Row}
374	when element(?vacmAStatus, Row) == ?'RowStatus_active'->
375	    case catch score(NextKey, GroupKey, ContextName,
376			     element(?vacmAContextMatch, Row),
377			     SecModel, SecLevel) of
378		{ok, NScore} when NScore > Score ->
379		    get_access_row(NextKey, GroupKey, ContextName,
380				   SecModel, SecLevel, NScore, Row);
381		{ok, _} -> % e.g. a throwed {ok, 0}
382		    get_access_row(NextKey, GroupKey, ContextName,
383				   SecModel, SecLevel, Score, Found);
384		false ->
385		    Found
386	    end;
387	{NextKey, _InvalidRow} ->
388	    get_access_row(NextKey, GroupKey, ContextName, SecModel,
389			   SecLevel, Score, Found);
390	false ->
391	    Found
392    end.
393
394
395
396score(Key, GroupKey, ContextName, Match, SecModel, SecLevel) ->
397    [CtxLen | Rest1] = chop_off_group(GroupKey, Key),
398    {NPrefix, [VSecModel, VSecLevel]} =
399	chop_off_context(ContextName, Rest1, 0, CtxLen, Match),
400    %% Make sure the vacmSecModel is valid (any or matching)
401    NSecModel = case VSecModel of
402		    SecModel -> 198;
403		    ?SEC_ANY -> 0;
404		    _        -> throw({ok, 0})
405		end,
406    %% Make sure the vacmSecLevel is less than the requested
407    NSecLevel =	if
408		    VSecLevel =< SecLevel -> VSecLevel - 1;
409		    true                  -> throw({ok, 0})
410		end,
411    {ok, NSecLevel + 3*CtxLen + NPrefix + NSecModel}.
412
413
414
415chop_off_group([H|T], [H|T2]) -> chop_off_group(T, T2);
416chop_off_group([], Rest) -> Rest;
417chop_off_group(_, _) -> throw(false).
418
419chop_off_context([H|T], [H|T2], Cnt, Len, Match) when Cnt < Len ->
420    chop_off_context(T, T2, Cnt+1, Len, Match);
421chop_off_context([], Rest, _Len, _Len, _Match) ->
422    %% We have exact match; don't care about Match
423    {99, Rest};
424chop_off_context(_, Rest, Len, Len, ?vacmAccessContextMatch_prefix) ->
425    %% We have a prefix match
426    {0, Rest};
427chop_off_context(_Ctx, _Rest, _Cnt, _Len, _Match) ->
428    %% Otherwise, it didn't match!
429    throw({ok, 0}).
430
431
432gc_tab(Oid) ->
433    case get_next_row(Oid) of
434	{NextOid, Row} ->
435	    case element(?vacmAStorageType, Row) of
436		?'StorageType_volatile' ->
437		    ets:delete(snmpa_vacm, NextOid),
438		    gc_tab(NextOid);
439		_ ->
440		    gc_tab(NextOid)
441	    end;
442	false ->
443	    ok
444    end.
445
446
447user_err(F, A) ->
448    snmpa_error:user_err(F, A).
449