%%
%% %CopyrightBegin%
%%
%% Copyright Ericsson AB 1999-2010. 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_vacm).
-export([get_mib_view/5]).
-export([init/1, init/2, backup/1]).
-export([delete/1, get_row/1, get_next_row/1, insert/1, insert/2,
cleanup/0, dump_table/0]).
-include("SNMPv2-TC.hrl").
-include("SNMP-VIEW-BASED-ACM-MIB.hrl").
-include("SNMP-FRAMEWORK-MIB.hrl").
-include("snmp_types.hrl").
-include("snmpa_vacm.hrl").
-define(VMODULE,"VACM").
-include("snmp_verbosity.hrl").
%%%-----------------------------------------------------------------
%%% Access Control Module for VACM (see also snmpa_acm)
%%% This module implements:
%%% 1. access control functions for VACM
%%% 2. vacmAccessTable as an ordered ets table
%%%
%%% This version of VACM handles v1, v2c and v3.
%%%-----------------------------------------------------------------
%%%-----------------------------------------------------------------
%%% 1. access control functions for VACM
%%%-----------------------------------------------------------------
%%-----------------------------------------------------------------
%% Func: get_mib_view/5 -> {ok, ViewName} |
%% {discarded, Reason}
%% Types: ViewType = read | write | notify
%% SecModel = ?SEC_* (see snmp_types.hrl)
%% SecName = string()
%% SecLevel = ?'SnmpSecurityLevel_*' (see SNMP-FRAMEWORK-MIB.hrl)
%% ContextName = string()
%% Purpose: This function is used to map VACM parameters to a mib
%% view.
%%-----------------------------------------------------------------
get_mib_view(ViewType, SecModel, SecName, SecLevel, ContextName) ->
check_auth(catch auth(ViewType, SecModel, SecName, SecLevel, ContextName)).
%% Follows the procedure in rfc2275
auth(ViewType, SecModel, SecName, SecLevel, ContextName) ->
% 3.2.1 - Check that the context is known to us
?vdebug("check that the context (~p) is known to us",[ContextName]),
case snmp_view_based_acm_mib:vacmContextTable(get, ContextName,
[?vacmContextName]) of
[_Found] ->
ok;
_ ->
snmpa_mpd:inc(snmpUnknownContexts),
throw({discarded, noSuchContext})
end,
% 3.2.2 - Check that the SecModel and SecName is valid
?vdebug("check that SecModel (~p) and SecName (~p) is valid",
[SecModel,SecName]),
GroupName =
case snmp_view_based_acm_mib:get(vacmSecurityToGroupTable,
[SecModel, length(SecName) | SecName],
[?vacmGroupName, ?vacmSecurityToGroupStatus]) of
[{value, GN}, {value, ?'RowStatus_active'}] ->
GN;
[{value, _GN}, {value, RowStatus}] ->
?vlog("valid SecModel and SecName but wrong row status:"
"~n RowStatus: ~p", [RowStatus]),
throw({discarded, noGroupName});
_ ->
throw({discarded, noGroupName})
end,
% 3.2.3-4 - Find an access entry and its view name
?vdebug("find an access entry and its view name",[]),
ViewName =
case get_view_name(ViewType, GroupName, ContextName,
SecModel, SecLevel) of
{ok, VN} -> VN;
Error -> throw(Error)
end,
% 3.2.5a - Find the corresponding mib view
?vdebug("find the corresponding mib view (for ~p)",[ViewName]),
get_mib_view(ViewName).
check_auth({'EXIT', Error}) -> exit(Error);
check_auth({discarded, Reason}) -> {discarded, Reason};
check_auth(Res) -> {ok, Res}.
%%-----------------------------------------------------------------
%% Returns a list of {ViewSubtree, ViewMask, ViewType}
%% The view table is index by ViewIndex, ViewSubtree,
%% so a next on ViewIndex returns the first
%% key in the table >= ViewIndex.
%%-----------------------------------------------------------------
get_mib_view(ViewName) ->
ViewKey = [length(ViewName) | ViewName],
case snmp_view_based_acm_mib:table_next(vacmViewTreeFamilyTable,
ViewKey) of
endOfTable ->
{discarded, noSuchView};
Indexes ->
case split_prefix(ViewKey, Indexes) of
{ok, Subtree} ->
loop_mib_view(ViewKey, Subtree, Indexes, []);
false ->
{discarded, noSuchView}
end
end.
split_prefix([H|T], [H|T2]) -> split_prefix(T,T2);
split_prefix([], Rest) -> {ok, Rest};
split_prefix(_, _) -> false.
%% ViewName is including length from now on
loop_mib_view(ViewName, Subtree, Indexes, MibView) ->
[{value, Mask}, {value, Type}, {value, Status}] =
snmp_view_based_acm_mib:vacmViewTreeFamilyTable(
get, Indexes,
[?vacmViewTreeFamilyMask,
?vacmViewTreeFamilyType,
?vacmViewTreeFamilyStatus]),
NextMibView =
case Status of
?'RowStatus_active' ->
[_Length | Tree] = Subtree,
[{Tree, Mask, Type} | MibView];
_ ->
MibView
end,
case snmp_view_based_acm_mib:table_next(vacmViewTreeFamilyTable,
Indexes) of
endOfTable -> NextMibView;
NextIndexes ->
case split_prefix(ViewName, NextIndexes) of
{ok, NextSubTree} ->
loop_mib_view(ViewName, NextSubTree, NextIndexes,
NextMibView);
false ->
NextMibView
end
end.
%%%-----------------------------------------------------------------
%%% 1b. The ordered ets table that implements vacmAccessTable
%%%-----------------------------------------------------------------
init(Dir) ->
init(Dir, terminate).
init(Dir, InitError) ->
FName = filename:join(Dir, "snmpa_vacm.db"),
case file:read_file_info(FName) of
{ok, _} ->
%% File exists - we must check this, since ets doesn't tell
%% us the reason in case of error...
case ets:file2tab(FName) of
{ok, _Tab} ->
gc_tab([]);
{error, Reason} ->
user_err("Corrupt VACM database ~p", [FName]),
case InitError of
terminate ->
throw({error, {file2tab, FName, Reason}});
_ ->
%% Rename old file (for later analyzes)
Saved = FName ++ ".saved",
file:rename(FName, Saved),
ets:new(snmpa_vacm,
[public, ordered_set, named_table])
end
end;
{error, _} ->
ets:new(snmpa_vacm, [public, ordered_set, named_table])
end,
ets:insert(snmp_agent_table, {snmpa_vacm_file, FName}),
{ok, FName}.
backup(BackupDir) ->
BackupFile = filename:join(BackupDir, "snmpa_vacm.db"),
ets:tab2file(snmpa_vacm, BackupFile).
%% Ret: {ok, ViewName} | {error, Reason}
get_view_name(ViewType, GroupName, ContextName, SecModel, SecLevel) ->
GroupKey = [length(GroupName) | GroupName],
case get_access_row(GroupKey, ContextName, SecModel, SecLevel) of
undefined ->
{discarded, noAccessEntry};
Row ->
?vtrace("get_view_name -> Row: ~n ~p", [Row]),
ViewName =
case ViewType of
read -> element(?vacmAReadViewName, Row);
write -> element(?vacmAWriteViewName, Row);
notify -> element(?vacmANotifyViewName, Row)
end,
case ViewName of
"" ->
?vtrace("get_view_name -> not found when"
"~n ViewType: ~p"
"~n GroupName: ~p"
"~n ContextName: ~p"
"~n SecModel: ~p"
"~n SecLevel: ~p", [ViewType, GroupName,
ContextName, SecModel,
SecLevel]),
{discarded, noSuchView};
_ -> {ok, ViewName}
end
end.
get_row(Key) ->
case ets:lookup(snmpa_vacm, Key) of
[{_Key, Row}] -> {ok, Row};
_ -> false
end.
get_next_row(Key) ->
case ets:next(snmpa_vacm, Key) of
'$end_of_table' -> false;
NextKey ->
case ets:lookup(snmpa_vacm, NextKey) of
[Entry] -> Entry;
_ -> false
end
end.
insert(Entries) -> insert(Entries, true).
insert(Entries, Dump) ->
lists:foreach(fun(Entry) -> ets:insert(snmpa_vacm, Entry) end, Entries),
dump_table(Dump).
delete(Key) ->
ets:delete(snmpa_vacm, Key),
dump_table().
cleanup() ->
ets:delete_all_objects(snmpa_vacm),
dump_table().
dump_table(true) ->
dump_table();
dump_table(_) ->
ok.
%% We should really make an effort to serialize the dumping
%% to ensure that several processes that dump at-the-same-time
%% do not trash each others dumps.
%% <SUGGESTION>
%% Send the request to the master agent, which, if there is no
%% dumper already running, spawns a (temporary) dumper process.
%% If there is already a running dumper process, instead increment
%% the dump_request counter.
%% When the dumper process exits, the master agent checks the
%% the dump_request counter, and if that is greater than zero,
%% create another dumper process and resets the counter.
%% In this way the dumping is serialized, but the master-agent
%% process is not burdend by the dumping.
%% </SUGGESTION>
dump_table() ->
%% The dumper fun is executed in a specially started process,
%% that does that one thing and then exits.
%% Also, to prevent the system to run "wild" (keep calling
%% dump function before they are done), the agents serialize
%% function return when that dump is done!
Dumper =
fun() ->
[{_, FName}] = ets:lookup(snmp_agent_table, snmpa_vacm_file),
%% TmpName = FName ++ ".tmp",
TmpName = unique_name(FName),
case ets:tab2file(snmpa_vacm, TmpName) of
ok ->
case file:rename(TmpName, FName) of
ok ->
ok;
Else -> % What is this? Undocumented return code...
user_err("Warning: could not move VACM db ~p"
" (~p)", [FName, Else])
end;
{error, Reason} ->
user_err("Warning: could not save vacm db ~p (~p)",
[FName, Reason])
end
end,
snmpa_agent:serialize(snmpa_vacm_dump_request, Dumper).
%% This little thing is an attempt to create a "unique" filename
%% in order to minimize the risk of two processes at the same
%% time dumping the table.
%% The serialization handled by the agent does this much better,
%% but this also gives us a "timestamp" which could be usefull for
%% debugging reasons.
unique_name(Pre) ->
unique_name(Pre, os:timestamp()).
unique_name(Pre, {_A, _B, C} = Timestamp) ->
{Date, Time} = calendar:now_to_datetime(Timestamp),
{YYYY, MM, DD} = Date,
{Hour, Min, Sec} = Time,
FormatDate =
io_lib:format("~.4w~.2.0w~.2.0w_~.2.0w~.2.0w~.2.0w_~w",
[YYYY, MM, DD, Hour, Min, Sec, round(C/1000)]),
unique_name2(Pre, FormatDate).
unique_name2(Pre, FormatedDate) ->
PidPart = unique_pid(),
lists:flatten(io_lib:format("~s.~s~s.tmp", [Pre, PidPart, FormatedDate])).
unique_pid() ->
case string:tokens(pid_to_list(self()), [$<,$.,$>]) of
[A, B, C] ->
A ++ B ++ C ++ ".";
_ ->
""
end.
%%-----------------------------------------------------------------
%% Alg.
%% Procedure is defined in the descr. of vacmAccessTable.
%%
%% for (each entry with matching group name, context, secmodel and seclevel)
%% {
%% rate the entry; if it's score is > prev max score, keep it
%% }
%%
%% Rating: The procedure says to keep entries in order
%% 1. matching secmodel ('any'(0) or same(1) is ok)
%% 2. matching contextprefix (exact(1) or prefix(0) is ok)
%% 3. longest prefix (0..32)
%% 4. highest secLevel (noAuthNoPriv(0) < authNoPriv(1) < authPriv(2))
%% We give each entry a single rating number according to this order.
%% The number is chosen so that a higher number gives a better
%% entry, according to the order above.
%% The number is:
%% secLevel + (3 * prefix_len) + (99 * match_prefix) + (198 * match_secmodel)
%%
%% Optimisation: Maybe the most common case is that there
%% is just one matching entry, and it matches exact. We could do
%% an exact lookup for this entry; if we find one, use it, otherwise
%% perform this alg.
%%-----------------------------------------------------------------
get_access_row(GroupKey, ContextName, SecModel, SecLevel) ->
%% First, try the optimisation...
ExactKey =
GroupKey ++ [length(ContextName) | ContextName] ++ [SecModel,SecLevel],
case ets:lookup(snmpa_vacm, ExactKey) of
[{_Key, Row}] ->
Row;
_ -> % Otherwise, perform the alg
get_access_row(GroupKey, GroupKey, ContextName,
SecModel, SecLevel, 0, undefined)
end.
get_access_row(Key, GroupKey, ContextName, SecModel, SecLevel, Score, Found) ->
case get_next_row(Key) of
{NextKey, Row}
when element(?vacmAStatus, Row) == ?'RowStatus_active'->
case catch score(NextKey, GroupKey, ContextName,
element(?vacmAContextMatch, Row),
SecModel, SecLevel) of
{ok, NScore} when NScore > Score ->
get_access_row(NextKey, GroupKey, ContextName,
SecModel, SecLevel, NScore, Row);
{ok, _} -> % e.g. a throwed {ok, 0}
get_access_row(NextKey, GroupKey, ContextName,
SecModel, SecLevel, Score, Found);
false ->
Found
end;
{NextKey, _InvalidRow} ->
get_access_row(NextKey, GroupKey, ContextName, SecModel,
SecLevel, Score, Found);
false ->
Found
end.
score(Key, GroupKey, ContextName, Match, SecModel, SecLevel) ->
[CtxLen | Rest1] = chop_off_group(GroupKey, Key),
{NPrefix, [VSecModel, VSecLevel]} =
chop_off_context(ContextName, Rest1, 0, CtxLen, Match),
%% Make sure the vacmSecModel is valid (any or matching)
NSecModel = case VSecModel of
SecModel -> 198;
?SEC_ANY -> 0;
_ -> throw({ok, 0})
end,
%% Make sure the vacmSecLevel is less than the requested
NSecLevel = if
VSecLevel =< SecLevel -> VSecLevel - 1;
true -> throw({ok, 0})
end,
{ok, NSecLevel + 3*CtxLen + NPrefix + NSecModel}.
chop_off_group([H|T], [H|T2]) -> chop_off_group(T, T2);
chop_off_group([], Rest) -> Rest;
chop_off_group(_, _) -> throw(false).
chop_off_context([H|T], [H|T2], Cnt, Len, Match) when Cnt < Len ->
chop_off_context(T, T2, Cnt+1, Len, Match);
chop_off_context([], Rest, _Len, _Len, _Match) ->
%% We have exact match; don't care about Match
{99, Rest};
chop_off_context(_, Rest, Len, Len, ?vacmAccessContextMatch_prefix) ->
%% We have a prefix match
{0, Rest};
chop_off_context(_Ctx, _Rest, _Cnt, _Len, _Match) ->
%% Otherwise, it didn't match!
throw({ok, 0}).
gc_tab(Oid) ->
case get_next_row(Oid) of
{NextOid, Row} ->
case element(?vacmAStorageType, Row) of
?'StorageType_volatile' ->
ets:delete(snmpa_vacm, NextOid),
gc_tab(NextOid);
_ ->
gc_tab(NextOid)
end;
false ->
ok
end.
user_err(F, A) ->
snmpa_error:user_err(F, A).
% config_err(F, A) ->
% snmpa_error:config_err(F, A).