aboutsummaryrefslogtreecommitdiffstats
path: root/lib/snmp/src/agent/snmp_user_based_sm_mib.erl
diff options
context:
space:
mode:
Diffstat (limited to 'lib/snmp/src/agent/snmp_user_based_sm_mib.erl')
-rw-r--r--lib/snmp/src/agent/snmp_user_based_sm_mib.erl1182
1 files changed, 1182 insertions, 0 deletions
diff --git a/lib/snmp/src/agent/snmp_user_based_sm_mib.erl b/lib/snmp/src/agent/snmp_user_based_sm_mib.erl
new file mode 100644
index 0000000000..7b881f888c
--- /dev/null
+++ b/lib/snmp/src/agent/snmp_user_based_sm_mib.erl
@@ -0,0 +1,1182 @@
+%%
+%% %CopyrightBegin%
+%%
+%% Copyright Ericsson AB 1999-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(snmp_user_based_sm_mib).
+
+-export([configure/1, reconfigure/1,
+ usmUserSpinLock/1, usmUserSpinLock/2,
+ usmUserTable/1, usmUserTable/3,
+ table_next/2,
+ is_engine_id_known/1, get_user/2, get_user_from_security_name/2,
+ mk_key_change/3, mk_key_change/5, extract_new_key/3, mk_random/1]).
+-export([add_user/1, add_user/13, delete_user/1]).
+
+%% Internal
+-export([check_usm/1]).
+
+-include("SNMP-USER-BASED-SM-MIB.hrl").
+-include("SNMP-USM-AES-MIB.hrl").
+-include("SNMPv2-TC.hrl").
+-include("snmpa_internal.hrl").
+-include("snmp_types.hrl").
+
+-define(VMODULE,"USM_MIB").
+-include("snmp_verbosity.hrl").
+
+-ifndef(default_verbosity).
+-define(default_verbosity,silence).
+-endif.
+
+
+%% Columns not accessible via SNMP
+-define(usmUserAuthKey, 14).
+-define(usmUserPrivKey, 15).
+
+
+%%%-----------------------------------------------------------------
+%%% Utility functions
+%%%-----------------------------------------------------------------
+
+%%%-----------------------------------------------------------------
+%%% Implements the instrumentation functions and additional
+%%% functions for the SNMP-USER-BASED-SM-MIB.
+%%%-----------------------------------------------------------------
+
+%%-----------------------------------------------------------------
+%% Func: configure/1
+%% Args: Dir is the directory where the configuration files are found.
+%% Purpose: If the tables doesn't exist, this function reads
+%% the config-files for the USM tables, and
+%% inserts the data. This means that the data in the tables
+%% survive a reboot. However, the StorageType column is
+%% checked for each row. If volatile, the row is deleted.
+%% Returns: ok
+%% Fails: exit(configuration_error)
+%%-----------------------------------------------------------------
+configure(Dir) ->
+ set_sname(),
+ case db(usmUserTable) of
+ {_, mnesia} ->
+ ?vdebug("usm user table in mnesia: init vars & cleanup",[]),
+ init_vars(),
+ gc_tabs();
+ TabDb ->
+ case snmpa_local_db:table_exists(TabDb) of
+ true ->
+ ?vdebug("usm user table exists: init vars & cleanup",[]),
+ init_vars(),
+ gc_tabs();
+ false ->
+ ?vdebug("usm user table does not exist: reconfigure",[]),
+ reconfigure(Dir)
+ end
+ end.
+
+%%-----------------------------------------------------------------
+%% Func: reconfigure/1
+%% Args: Dir is the directory where the configuration files are found.
+%% Purpose: Reads the config-files for the USM tables, and
+%% inserts the data. Makes sure that all old data in
+%% the tables are deleted, and the new data inserted.
+%% This function makes sure that all (and only)
+%% config-file-data are in the tables.
+%% Returns: ok
+%% Fails: exit(configuration_error) |
+%% exit({unsupported_crypto, Function})
+%%-----------------------------------------------------------------
+reconfigure(Dir) ->
+ set_sname(),
+ case (catch do_reconfigure(Dir)) of
+ ok ->
+ ok;
+ {error, Reason} ->
+ ?vinfo("reconfigure error: ~p", [Reason]),
+ config_err("reconfigure failed: ~p", [Reason]),
+ exit(configuration_error);
+ Error ->
+ ?vinfo("reconfigure failed: ~p", [Error]),
+ config_err("reconfigure failed: ~p", [Error]),
+ exit(configuration_error)
+ end.
+
+do_reconfigure(Dir) ->
+ ?vdebug("read usm configuration files",[]),
+ Users = read_usm_config_files(Dir),
+ ?vdebug("check users",[]),
+ check_users(Users),
+ ?vdebug("initiate tables",[]),
+ init_tabs(Users),
+ ?vdebug("initiate vars",[]),
+ init_vars(),
+ ok.
+
+
+read_usm_config_files(Dir) ->
+ ?vdebug("read usm config file",[]),
+ Gen = fun(D) -> generate_usm(D) end,
+ Filter = fun(Usms) -> Usms end,
+ Check = fun(Entry) -> check_usm(Entry) end,
+ [Usms] =
+ snmp_conf:read_files(Dir, [{Gen, Filter, Check, "usm.conf"}]),
+ Usms.
+
+
+generate_usm(Dir) ->
+ info_msg("Incomplete configuration. Generating empty usm.conf.", []),
+ USMFile = filename:join(Dir, "usm.conf"),
+ ok = file:write_file(USMFile, list_to_binary([])).
+
+
+check_usm({EngineID, Name, SecName, Clone, AuthP, AuthKeyC, OwnAuthKeyC,
+ PrivP, PrivKeyC, OwnPrivKeyC, Public, AuthKey, PrivKey}) ->
+ snmp_conf:check_string(EngineID),
+ snmp_conf:check_string(Name),
+ snmp_conf:check_string(SecName),
+ CloneVal =
+ case catch snmp_conf:check_atom(Clone,[{zeroDotZero,?zeroDotZero}]) of
+ {error, _} ->
+ snmp_conf:check_oid(Clone),
+ Clone;
+ {ok, X} ->
+ X
+ end,
+ AuthProtoAlt = [{usmNoAuthProtocol, ?usmNoAuthProtocol},
+ {usmHMACSHAAuthProtocol, ?usmHMACSHAAuthProtocol},
+ {usmHMACMD5AuthProtocol, ?usmHMACMD5AuthProtocol}],
+ {ok, AuthProto} = snmp_conf:check_atom(AuthP, AuthProtoAlt),
+ snmp_conf:check_string(AuthKeyC),
+ snmp_conf:check_string(OwnAuthKeyC),
+ PrivProtoAlt = [{usmNoPrivProtocol, ?usmNoPrivProtocol},
+ {usmDESPrivProtocol, ?usmDESPrivProtocol},
+ {usmAesCfb128Protocol, ?usmAesCfb128Protocol}],
+ {ok, PrivProto} = snmp_conf:check_atom(PrivP, PrivProtoAlt),
+ snmp_conf:check_string(PrivKeyC),
+ snmp_conf:check_string(OwnPrivKeyC),
+ snmp_conf:check_string(Public),
+ snmp_conf:check_string(AuthKey, alen(AuthP)),
+ snmp_conf:check_string(PrivKey, plen(PrivP)),
+
+ User = {EngineID, Name, SecName,
+ CloneVal, AuthProto, AuthKeyC, OwnAuthKeyC,
+ PrivProto, PrivKeyC, OwnPrivKeyC, Public,
+ ?'StorageType_nonVolatile', ?'RowStatus_active', AuthKey, PrivKey},
+ {ok, User};
+check_usm(X) ->
+ error({invalid_user, X}).
+
+alen(usmNoAuthProtocol) -> any;
+alen(usmHMACMD5AuthProtocol) -> 16;
+alen(usmHMACSHAAuthProtocol) -> 20.
+
+plen(usmNoPrivProtocol) -> any;
+plen(usmDESPrivProtocol) -> 16;
+plen(usmAesCfb128Protocol) -> 16.
+
+
+%%-----------------------------------------------------------------
+%% This function loops through all users, and check that the
+%% definition is constistent with the support for crypto on
+%% the system. Thus, it is not possible to define a user that
+%% uses authentication if 'crypto' is not started, or a user that
+%% uses DES if 'crypto' doesn't support DES.
+%%-----------------------------------------------------------------
+check_users([User | Users]) ->
+ check_user(User),
+ check_users(Users);
+check_users([]) ->
+ ok.
+
+check_user(User) ->
+ case element(?usmUserAuthProtocol, User) of
+ ?usmNoAuthProtocol -> ok;
+ ?usmHMACMD5AuthProtocol ->
+ case is_crypto_supported(md5_mac_96) of
+ true -> ok;
+ false -> exit({unsupported_crypto, md5_mac_96})
+ end;
+ ?usmHMACSHAAuthProtocol ->
+ case is_crypto_supported(sha_mac_96) of
+ true -> ok;
+ false -> exit({unsupported_crypto, sha_mac_96})
+ end
+ end,
+ case element(?usmUserPrivProtocol, User) of
+ ?usmNoPrivProtocol -> ok;
+ ?usmDESPrivProtocol ->
+ case is_crypto_supported(des_cbc_decrypt) of
+ true -> ok;
+ false -> exit({unsupported_crypto, des_cbc_decrypt})
+ end;
+ ?usmAesCfb128Protocol ->
+ case is_crypto_supported(aes_cfb_128_decrypt) of
+ true -> ok;
+ false -> exit({unsupported_crypto, aes_cfb_128_decrypt})
+ end
+ end.
+
+
+init_tabs(Users) ->
+ ?vdebug("create usm user table",[]),
+ snmpa_local_db:table_delete(db(usmUserTable)),
+ snmpa_local_db:table_create(db(usmUserTable)),
+ init_user_table(Users).
+
+init_user_table([Row | T]) ->
+ Key = mk_user_table_key(Row),
+ snmpa_local_db:table_create_row(db(usmUserTable), Key, Row),
+ init_user_table(T);
+init_user_table([]) -> true.
+
+mk_user_table_key(Row) ->
+ Key1 = element(1, Row),
+ Key2 = element(2, Row),
+ [length(Key1) | Key1] ++ [length(Key2) | Key2].
+
+table_cre_row(Tab, Key, Row) ->
+ snmpa_mib_lib:table_cre_row(db(Tab), Key, Row).
+
+table_del_row(Tab, Key) ->
+ snmpa_mib_lib:table_del_row(db(Tab), Key).
+
+
+add_user(EngineID, Name, SecName, Clone, AuthP, AuthKeyC, OwnAuthKeyC,
+ PrivP, PrivKeyC, OwnPrivKeyC, Public, AuthKey, PrivKey) ->
+ User = {EngineID, Name, SecName, Clone, AuthP, AuthKeyC, OwnAuthKeyC,
+ PrivP, PrivKeyC, OwnPrivKeyC, Public, AuthKey, PrivKey},
+ add_user(User).
+
+add_user(User) ->
+ case (catch check_usm(User)) of
+ {ok, Row} ->
+ case (catch check_user(Row)) of
+ {'EXIT', Reason} ->
+ {error, Reason};
+ _ ->
+ Key = mk_user_table_key(Row),
+ case table_cre_row(usmUserTable, Key, Row) of
+ true ->
+ {ok, Key};
+ false ->
+ {error, create_failed}
+ end
+ end;
+ {error, Reason} ->
+ {error, Reason};
+ Error ->
+ {error, Error}
+ end.
+
+delete_user(Key) ->
+ case table_del_row(usmUserTable, Key) of
+ true ->
+ ok;
+ false ->
+ {error, delete_failed}
+ end.
+
+
+gc_tabs() ->
+ DB = db(usmUserTable),
+ STC = stc(usmUserTable),
+ FOI = foi(usmUserTable),
+ snmpa_mib_lib:gc_tab(DB, STC, FOI),
+ ok.
+
+
+%%-----------------------------------------------------------------
+%% Counter functions
+%%-----------------------------------------------------------------
+init_vars() -> lists:map(fun maybe_create_var/1, vars()).
+
+maybe_create_var(Var) ->
+ case ets:lookup(snmp_agent_table, Var) of
+ [_] -> ok;
+ _ -> init_var(Var)
+ end.
+
+init_var(Var) -> ets:insert(snmp_agent_table, {Var, 0}).
+
+vars() ->
+ [
+ usmStatsUnsupportedSecLevels,
+ usmStatsNotInTimeWindows,
+ usmStatsUnknownUserNames,
+ usmStatsUnknownEngineIDs,
+ usmStatsWrongDigests,
+ usmStatsDecryptionErrors
+ ].
+
+%%-----------------------------------------------------------------
+%% API functions
+%%-----------------------------------------------------------------
+is_engine_id_known(EngineID) ->
+ EngineKey = [length(EngineID) | EngineID],
+ ?vtrace("is_engine_id_known -> EngineKey: ~w", [EngineKey]),
+ case table_next(usmUserTable, EngineKey) of
+ endOfTable -> false;
+ Key ->
+ ?vtrace("is_engine_id_known -> Key: ~w", [Key]),
+ lists:prefix(EngineKey, Key)
+ end.
+
+get_user(EngineID, UserName) ->
+ Key = [length(EngineID) | EngineID] ++ [length(UserName) | UserName],
+ snmp_generic:table_get_row(db(usmUserTable), Key, foi(usmUserTable)).
+
+get_user_from_security_name(EngineID, SecName) ->
+ %% Since the normal mapping between UserName and SecName is the
+ %% identityfunction, we first try to use the SecName as UserName,
+ %% and check the resulting row. If it doesn't match, we'll have to
+ %% loop through the entire table.
+ Key = [length(EngineID) | EngineID] ++ [length(SecName) | SecName],
+ case snmp_generic:table_get_row(db(usmUserTable), Key,
+ foi(usmUserTable)) of
+ Row when is_tuple(Row) ->
+ ?vtrace("get_user_from_security_name -> "
+ "found user using the identity function", []),
+ Row;
+ undefined ->
+ ?vtrace("get_user_from_security_name -> "
+ "user *not* found using the identity function", []),
+ F = fun(_, Row) when (
+ (element(?usmUserEngineID,Row) =:= EngineID) andalso
+ (element(?usmUserSecurityName,Row) =:= SecName)) ->
+ throw({ok, Row});
+ (_, _) ->
+ ok
+ end,
+ case catch snmp_generic:table_foreach(db(usmUserTable), F) of
+ {ok, Row} ->
+ Row;
+ _Else ->
+ undefined
+ end
+ end.
+
+
+%%-----------------------------------------------------------------
+%% Instrumentation Functions
+%%-----------------------------------------------------------------
+usmUserSpinLock(new) ->
+ snmp_generic:variable_func(new, {usmUserSpinLock, volatile}),
+ {A1,A2,A3} = erlang:now(),
+ random:seed(A1,A2,A3),
+ Val = random:uniform(2147483648) - 1,
+ snmp_generic:variable_func(set, Val, {usmUserSpinLock, volatile});
+
+usmUserSpinLock(delete) ->
+ ok;
+
+usmUserSpinLock(get) ->
+ snmp_generic:variable_func(get, {usmUserSpinLock, volatile}).
+
+usmUserSpinLock(is_set_ok, NewVal) ->
+ case snmp_generic:variable_func(get, {usmUserSpinLock, volatile}) of
+ {value, NewVal} -> noError;
+ _ -> inconsistentValue
+ end;
+usmUserSpinLock(set, NewVal) ->
+ snmp_generic:variable_func(set, (NewVal + 1) rem 2147483648,
+ {usmUserSpinLock, volatile}).
+
+
+%% Op = print - Used for debugging purposes
+usmUserTable(print) ->
+ Table = usmUserTable,
+ DB = db(Table),
+ FOI = foi(Table),
+ %% TableInfo = snmpa_mib_lib:get_table(db(Table), foi(Table)),
+ PrintRow =
+ fun(Prefix, Row) ->
+ lists:flatten(
+ io_lib:format("~sEngineID: ~p"
+ "~n~sName: ~p"
+ "~n~sSecurityName: ~p"
+ "~n~sCloneFrom: ~p"
+ "~n~sAuthProtocol: ~p (~w)"
+ "~n~sAuthKeyChange: ~p"
+ "~n~sOwnAuthKeyChange: ~p"
+ "~n~sPrivProtocol: ~p (~w)"
+ "~n~sPrivKeyChange: ~p"
+ "~n~sOwnPrivKeyChange: ~p"
+ "~n~sPublic: ~p"
+ "~n~sStorageType: ~p (~w)"
+ "~n~sStatus: ~p (~w)",
+ [Prefix, element(?usmUserEngineID, Row),
+ Prefix, element(?usmUserName, Row),
+ Prefix, element(?usmUserSecurityName, Row),
+ Prefix, element(?usmUserCloneFrom, Row),
+ Prefix, element(?usmUserAuthProtocol, Row),
+ case element(?usmUserAuthProtocol, Row) of
+ ?usmNoAuthProtocol -> none;
+ ?usmHMACMD5AuthProtocol -> md5;
+ ?usmHMACSHAAuthProtocol -> sha;
+ md5 -> md5;
+ sha -> sha;
+ _ -> undefined
+ end,
+ Prefix, element(?usmUserAuthKeyChange, Row),
+ Prefix, element(?usmUserOwnAuthKeyChange, Row),
+ Prefix, element(?usmUserPrivProtocol, Row),
+ case element(?usmUserPrivProtocol, Row) of
+ ?usmNoPrivProtocol -> none;
+ ?usmDESPrivProtocol -> des;
+ ?usmAesCfb128Protocol -> aes;
+ des -> des;
+ aes -> aes;
+ _ -> undefined
+ end,
+ Prefix, element(?usmUserPrivKeyChange, Row),
+ Prefix, element(?usmUserOwnPrivKeyChange, Row),
+ Prefix, element(?usmUserPublic, Row),
+ Prefix, element(?usmUserStorageType, Row),
+ case element(?usmUserStorageType, Row) of
+ ?'usmUserStorageType_readOnly' -> readOnly;
+ ?'usmUserStorageType_permanent' -> permanent;
+ ?'usmUserStorageType_nonVolatile' -> nonVolatile;
+ ?'usmUserStorageType_volatile' -> volatile;
+ ?'usmUserStorageType_other' -> other;
+ _ -> undefined
+ end,
+ Prefix, element(?usmUserStatus, Row),
+ case element(?usmUserStatus, Row) of
+ ?'usmUserStatus_destroy' -> destroy;
+ ?'usmUserStatus_createAndWait' -> createAndWait;
+ ?'usmUserStatus_createAndGo' -> createAndGo;
+ ?'usmUserStatus_notReady' -> notReady;
+ ?'usmUserStatus_notInService' -> notInService;
+ ?'usmUserStatus_active' -> active;
+ _ -> undefined
+ end]))
+ end,
+ snmpa_mib_lib:print_table(Table, DB, FOI, PrintRow);
+%% Op == new | delete
+usmUserTable(Op) ->
+ snmp_generic:table_func(Op, db(usmUserTable)).
+
+%% Op == get | is_set_ok | set | get_next
+usmUserTable(get, RowIndex, Cols) ->
+ get_patch(Cols, get(usmUserTable, RowIndex, Cols));
+usmUserTable(get_next, RowIndex, Cols) ->
+ next_patch(next(usmUserTable, RowIndex, Cols));
+usmUserTable(is_set_ok, RowIndex, Cols0) ->
+ ?vtrace("usmUserTable(is_set_ok) -> entry with"
+ "~n RowIndex: ~p"
+ "~n Cols0: ~p", [RowIndex, Cols0]),
+ case (catch verify_usmUserTable_cols(Cols0, [])) of
+ {ok, Cols} ->
+ ?vtrace("usmUserTable(is_set_ok) -> verified: "
+ "~n Cols: ~p", [Cols]),
+ %% Add a dummy value for securityName; otherwise snmp_generic will
+ %% think that a value is missing, so the row can't be created.
+ %% Note: this value is only added for is_set_ok, not for set!
+ NCols = [{?usmUserSecurityName, ""} | Cols],
+ IsSetOkRes = snmp_generic:table_func(is_set_ok, RowIndex,
+ NCols, db(usmUserTable)),
+ ?vtrace("usmUserTable(is_set_ok) -> tested: "
+ "~n IsSetOkRes: ~p", [IsSetOkRes]),
+ validate_is_set_ok(IsSetOkRes, RowIndex, Cols);
+ Error ->
+ Error
+ end;
+usmUserTable(set, RowIndex, Cols0) ->
+ ?vtrace("usmUserTable(set) -> entry with"
+ "~n RowIndex: ~p"
+ "~n Cols0: ~p", [RowIndex, Cols0]),
+ case (catch verify_usmUserTable_cols(Cols0, [])) of
+ {ok, Cols} ->
+ ?vtrace("usmUserTable(set) -> verified"
+ "~n Cols: ~p", [Cols]),
+ NCols = pre_set(RowIndex, Cols),
+ ?vtrace("usmUserTable(set) -> pre-set: "
+ "~n NCols: ~p", [NCols]),
+ %% NOTE: The NCols parameter is sent to snmp_generic, but not to
+ %% validate_set! The reason is that the columns from pre_set are
+ %% set in snmp_generic, but not used by validate_set.
+ validate_set(snmp_generic:table_func(set, RowIndex,
+ NCols, db(usmUserTable)),
+ RowIndex, Cols);
+ Error ->
+ Error
+ end;
+usmUserTable(Op, Arg1, Arg2) ->
+ snmp_generic:table_func(Op, Arg1, Arg2, db(usmUserTable)).
+
+
+verify_usmUserTable_cols([], Cols) ->
+ ?vtrace("verify_usmUserTable_cols -> entry when done with"
+ "~n Cols: ~p", [Cols]),
+ {ok, lists:reverse(Cols)};
+verify_usmUserTable_cols([{Col, Val0}|Cols], Acc) ->
+ ?vtrace("verify_usmUserTable_cols -> entry with"
+ "~n Col: ~p"
+ "~n Val0: ~p", [Col, Val0]),
+ Val = verify_usmUserTable_col(Col, Val0),
+ ?vtrace("verify_usmUserTable_cols -> verified: "
+ "~n Val: ~p", [Val]),
+ verify_usmUserTable_cols(Cols, [{Col, Val}|Acc]).
+
+verify_usmUserTable_col(?usmUserEngineID, EngineID) ->
+ case (catch snmp_conf:check_string(EngineID)) of
+ ok ->
+ EngineID;
+ _ ->
+ wrongValue(?usmUserEngineID)
+ end;
+verify_usmUserTable_col(?usmUserName, Name) ->
+ case (catch snmp_conf:check_string(Name)) of
+ ok ->
+ Name;
+ _ ->
+ wrongValue(?usmUserName)
+ end;
+verify_usmUserTable_col(?usmUserSecurityName, Name) ->
+ case (catch snmp_conf:check_string(Name)) of
+ ok ->
+ Name;
+ _ ->
+ wrongValue(?usmUserSecurityName)
+ end;
+verify_usmUserTable_col(?usmUserCloneFrom, Clone) ->
+ case Clone of
+ zeroDotZero -> ?zeroDotZero;
+ ?zeroDotZero -> ?zeroDotZero;
+ _ ->
+ case (catch snmp_conf:check_oid(Clone)) of
+ ok ->
+ Clone;
+ _ ->
+ wrongValue(?usmUserCloneFrom)
+ end
+ end;
+verify_usmUserTable_col(?usmUserAuthProtocol, AuthP) ->
+ case AuthP of
+ usmNoAuthProtocol -> ?usmNoAuthProtocol;
+ usmHMACSHAAuthProtocol -> ?usmHMACSHAAuthProtocol;
+ usmHMACMD5AuthProtocol -> ?usmHMACMD5AuthProtocol;
+ ?usmNoAuthProtocol -> ?usmNoAuthProtocol;
+ ?usmHMACSHAAuthProtocol -> ?usmHMACSHAAuthProtocol;
+ ?usmHMACMD5AuthProtocol -> ?usmHMACMD5AuthProtocol;
+ _ ->
+ wrongValue(?usmUserAuthProtocol)
+ end;
+verify_usmUserTable_col(?usmUserAuthKeyChange, AKC) ->
+ case (catch snmp_conf:check_string(AKC)) of
+ ok ->
+ AKC;
+ _ ->
+ wrongValue(?usmUserAuthKeyChange)
+ end;
+verify_usmUserTable_col(?usmUserOwnAuthKeyChange, OAKC) ->
+ case (catch snmp_conf:check_string(OAKC)) of
+ ok ->
+ OAKC;
+ _ ->
+ wrongValue(?usmUserOwnAuthKeyChange)
+ end;
+verify_usmUserTable_col(?usmUserPrivProtocol, PrivP) ->
+ case PrivP of
+ usmNoPrivProtocol -> ?usmNoPrivProtocol;
+ usmDESPrivProtocol -> ?usmDESPrivProtocol;
+ usmAesCfb128Protocol -> ?usmAesCfb128Protocol;
+ ?usmNoPrivProtocol -> ?usmNoPrivProtocol;
+ ?usmDESPrivProtocol -> ?usmDESPrivProtocol;
+ ?usmAesCfb128Protocol -> ?usmAesCfb128Protocol;
+ _ ->
+ wrongValue(?usmUserPrivProtocol)
+ end;
+verify_usmUserTable_col(?usmUserPrivKeyChange, PKC) ->
+ case (catch snmp_conf:check_string(PKC)) of
+ ok ->
+ PKC;
+ _ ->
+ wrongValue(?usmUserPrivKeyChange)
+ end;
+verify_usmUserTable_col(?usmUserOwnPrivKeyChange, OPKC) ->
+ case (catch snmp_conf:check_string(OPKC)) of
+ ok ->
+ OPKC;
+ _ ->
+ wrongValue(?usmUserOwnPrivKeyChange)
+ end;
+verify_usmUserTable_col(?usmUserPublic, Public) ->
+ case (catch snmp_conf:check_string(Public)) of
+ ok ->
+ Public;
+ _ ->
+ wrongValue(?usmUserPublic)
+ end;
+verify_usmUserTable_col(_, Val) ->
+ Val.
+
+
+%% Patch the values stored in the DB with other values for some
+%% objects.
+get_patch([?usmUserCloneFrom | Cols], [{value, _Val} | Vals]) ->
+ [{value, ?zeroDotZero} | get_patch(Cols, Vals)];
+get_patch([?usmUserAuthKeyChange | Cols], [{value, _Val} | Vals]) ->
+ [{value, ""} | get_patch(Cols, Vals)];
+get_patch([?usmUserOwnAuthKeyChange | Cols], [{value, _Val} | Vals]) ->
+ [{value, ""} | get_patch(Cols, Vals)];
+get_patch([?usmUserPrivKeyChange | Cols], [{value, _Val} | Vals]) ->
+ [{value, ""} | get_patch(Cols, Vals)];
+get_patch([?usmUserOwnPrivKeyChange | Cols], [{value, _Val} | Vals]) ->
+ [{value, ""} | get_patch(Cols, Vals)];
+get_patch([_Col | Cols], [Val | Vals]) ->
+ [Val | get_patch(Cols, Vals)];
+get_patch(_Cols, Result) ->
+ Result.
+
+next_patch([{[?usmUserCloneFrom | Idx], _Val} | Vals]) ->
+ [{[?usmUserCloneFrom | Idx], ?zeroDotZero} | next_patch(Vals)];
+next_patch([{[?usmUserAuthKeyChange | Idx], _Val} | Vals]) ->
+ [{[?usmUserAuthKeyChange | Idx], ""} | next_patch(Vals)];
+next_patch([{[?usmUserOwnAuthKeyChange | Idx], _Val} | Vals]) ->
+ [{[?usmUserOwnAuthKeyChange | Idx], ""} | next_patch(Vals)];
+next_patch([{[?usmUserPrivKeyChange | Idx], _Val} | Vals]) ->
+ [{[?usmUserPrivKeyChange | Idx], ""} | next_patch(Vals)];
+next_patch([{[?usmUserOwnPrivKeyChange | Idx], _Val} | Vals]) ->
+ [{[?usmUserOwnPrivKeyChange | Idx], ""} | next_patch(Vals)];
+next_patch([Val | Vals]) ->
+ [Val | next_patch(Vals)];
+next_patch(Result) -> Result.
+
+
+validate_is_set_ok({noError, 0}, RowIndex, Cols) ->
+ case (catch do_validate_is_set_ok(RowIndex, Cols)) of
+ ok ->
+ {noError, 0};
+ Error ->
+ Error
+ end;
+validate_is_set_ok(Error, _RowIndex, _Cols) ->
+ Error.
+
+do_validate_is_set_ok(RowIndex, Cols) ->
+ validate_clone_from(RowIndex, Cols),
+ validate_auth_protocol(RowIndex, Cols),
+ validate_auth_key_change(RowIndex, Cols),
+ validate_own_auth_key_change(RowIndex, Cols),
+ validate_priv_protocol(RowIndex, Cols),
+ validate_priv_key_change(RowIndex, Cols),
+ validate_own_priv_key_change(RowIndex, Cols),
+ ok.
+
+pre_set(RowIndex, Cols) ->
+ %% Possibly initialize the usmUserSecurityName and privacy keys
+ case snmp_generic:table_row_exists(db(usmUserTable), RowIndex) of
+ true -> Cols;
+ false ->
+ SecName = get_user_name(RowIndex),
+ [{?usmUserSecurityName, SecName} | Cols] ++
+ [{?usmUserAuthKey, ""},
+ {?usmUserPrivKey, ""}]
+ end.
+
+validate_set({noError, 0}, RowIndex, Cols) ->
+ %% Now, all is_set_ok validation steps have been executed. So
+ %% everything is ready for the set.
+ set_clone_from(RowIndex, Cols),
+ set_auth_key_change(RowIndex, Cols),
+ set_own_auth_key_change(RowIndex, Cols),
+ set_priv_key_change(RowIndex, Cols),
+ set_own_priv_key_change(RowIndex, Cols),
+ {noError, 0};
+validate_set(Error, _RowIndex, _Cols) ->
+ Error.
+
+%%-----------------------------------------------------------------
+%% Here's the alg: If this is the first time the CloneFrom is written,
+%% we must check that the CloneFrom row exists, so we can invoke the
+%% clone process in the set phase. Otherwise, the set succed, with
+%% no further checks.
+%%-----------------------------------------------------------------
+validate_clone_from(RowIndex, Cols) ->
+ case lists:keysearch(?usmUserCloneFrom, 1, Cols) of
+ {value, {_Col, RowPointer}} ->
+ RowIndex2 = extract_row(RowPointer),
+ OldCloneFrom = snmp_generic:table_get_element(db(usmUserTable),
+ RowIndex,
+ ?usmUserCloneFrom),
+ case OldCloneFrom of
+ {value, Val} when Val /= noinit ->
+ %% This means that the cloning is already done...
+ ok;
+ _ ->
+ %% Otherwise, we must check the CloneFrom value
+ case snmp_generic:table_get_element(db(usmUserTable),
+ RowIndex2,
+ ?usmUserStatus) of
+ {value, ?'RowStatus_active'} -> ok;
+ _ -> inconsistentName(?usmUserCloneFrom)
+ end
+ end;
+ false ->
+ ok
+ end.
+
+
+validate_auth_protocol(RowIndex, Cols) ->
+ case lists:keysearch(?usmUserAuthProtocol, 1, Cols) of
+ {value, {_Col, AuthProtocol}} ->
+ %% Check if the row has been cloned; we can't check the
+ %% old value of authProtocol, because if the row was
+ %% createAndWaited, the default value would have been
+ %% written (usmNoAuthProtocol).
+ OldCloneFrom = snmp_generic:table_get_element(db(usmUserTable),
+ RowIndex,
+ ?usmUserCloneFrom),
+ case OldCloneFrom of
+ {value, Val} when Val /= noinit ->
+ %% This means that the cloning is already done; set is ok
+ %% if new protocol is usmNoAuthProtocol
+ case AuthProtocol of
+ ?usmNoAuthProtocol ->
+ %% Check that the Priv protocl is noPriv
+ case get_priv_proto(RowIndex, Cols) of
+ ?usmNoPrivProtocol -> ok;
+ _ -> inconsistentValue(?usmUserAuthProtocol)
+ end;
+ ?usmHMACMD5AuthProtocol ->
+ inconsistentValue(?usmUserAuthProtocol);
+ ?usmHMACSHAAuthProtocol ->
+ inconsistentValue(?usmUserAuthProtocol);
+ _ ->
+ wrongValue(?usmUserAuthProtocol)
+ end;
+ _ ->
+ %% Otherwise, check that the new protocol is known,
+ %% and that the system we're running supports the
+ %% hash function.
+ case AuthProtocol of
+ ?usmNoAuthProtocol ->
+ %% Check that the Priv protocl is noPriv
+ case get_priv_proto(RowIndex, Cols) of
+ ?usmNoPrivProtocol -> ok;
+ _ -> inconsistentValue(?usmUserAuthProtocol)
+ end;
+ ?usmHMACMD5AuthProtocol ->
+ case is_crypto_supported(md5_mac_96) of
+ true -> ok;
+ false ->
+ wrongValue(?usmUserAuthProtocol)
+ end;
+ ?usmHMACSHAAuthProtocol ->
+ case is_crypto_supported(sha_mac_96) of
+ true -> ok;
+ false ->
+ wrongValue(?usmUserAuthProtocol)
+ end;
+ _ -> wrongValue(?usmUserAuthProtocol)
+ end
+ end;
+ false ->
+ ok
+ end.
+
+validate_auth_key_change(RowIndex, Cols) ->
+ validate_key_change(RowIndex, Cols, ?usmUserAuthKeyChange, auth).
+
+validate_own_auth_key_change(RowIndex, Cols) ->
+ validate_requester(RowIndex, Cols, ?usmUserOwnAuthKeyChange),
+ validate_key_change(RowIndex, Cols, ?usmUserOwnAuthKeyChange, auth).
+
+validate_priv_key_change(RowIndex, Cols) ->
+ validate_key_change(RowIndex, Cols, ?usmUserPrivKeyChange, priv).
+
+validate_own_priv_key_change(RowIndex, Cols) ->
+ validate_requester(RowIndex, Cols, ?usmUserOwnPrivKeyChange),
+ validate_key_change(RowIndex, Cols, ?usmUserOwnPrivKeyChange, priv).
+
+%% Check that the requesting user is the same as the modified user
+validate_requester(RowIndex, Cols, KeyChangeCol) ->
+ case lists:keysearch(KeyChangeCol, 1, Cols) of
+ {value, _} ->
+ case get(sec_model) of % Check the securityModel in the request
+ ?SEC_USM -> ok;
+ _ -> noAccess(KeyChangeCol)
+ end,
+ %% The SecurityName may not be set yet. First, check if it is set.
+ SecNameForUser =
+ case snmp_generic:table_get_element(db(usmUserTable),
+ RowIndex,
+ ?usmUserSecurityName) of
+ {value, Val} when Val /= noinit -> Val;
+ _ -> get_user_name(RowIndex)
+ end,
+ case get(sec_name) of % Check the securityName in the request
+ SecNameForUser -> ok;
+ _ -> noAccess(KeyChangeCol)
+ end;
+ false ->
+ ok
+ end.
+
+validate_key_change(RowIndex, Cols, KeyChangeCol, Type) ->
+ case lists:keysearch(KeyChangeCol, 1, Cols) of
+ {value, {_Col, KeyC}} ->
+ %% Check if the row has been cloned; or if it is cloned in
+ %% this set-operation.
+ OldCloneFrom = snmp_generic:table_get_element(db(usmUserTable),
+ RowIndex,
+ ?usmUserCloneFrom),
+ IsClonePresent = case lists:keysearch(?usmUserCloneFrom, 1, Cols) of
+ {value, _} -> true;
+ false -> false
+ end,
+ %% Set is ok if 1) the user already is created, 2) this is
+ %% a new user, which has been cloned, or is about to be
+ %% cloned.
+ case {OldCloneFrom, IsClonePresent} of
+ {{value, Val}, _} when Val /= noinit ->
+ %% The user exists, or has been cloned
+ ok;
+ {_, true} ->
+ %% The user is cloned in this operation
+ ok;
+ _ ->
+ %% The user doen't exist, or hasn't been cloned,
+ %% and is not cloned in this operation.
+ inconsistentName(KeyChangeCol)
+ end,
+ %% Check that the length makes sense
+ Len = length(KeyC),
+ case Type of
+ auth ->
+ case get_auth_proto(RowIndex, Cols) of
+ ?usmNoAuthProtocol -> ok;
+ ?usmHMACMD5AuthProtocol when Len =:= 32 -> ok;
+ ?usmHMACSHAAuthProtocol when Len =:= 40 -> ok;
+ _ -> wrongValue(KeyChangeCol)
+ end;
+ priv ->
+ case get_priv_proto(RowIndex, Cols) of
+ ?usmNoPrivProtocol -> ok;
+ ?usmDESPrivProtocol when Len == 32 -> ok;
+ ?usmAesCfb128Protocol when Len == 32 -> ok;
+ _ -> wrongValue(KeyChangeCol)
+ end
+ end;
+ false ->
+ ok
+ end.
+
+validate_priv_protocol(RowIndex, Cols) ->
+ case lists:keysearch(?usmUserPrivProtocol, 1, Cols) of
+ {value, {_Col, PrivProtocol}} ->
+ %% Check if the row has been cloned; we can't check the
+ %% old value of privhProtocol, because if the row was
+ %% createAndWaited, the default value would have been
+ %% written (usmNoPrivProtocol).
+ OldCloneFrom = snmp_generic:table_get_element(db(usmUserTable),
+ RowIndex,
+ ?usmUserCloneFrom),
+ case OldCloneFrom of
+ {value, Val} when Val /= noinit ->
+ %% This means that the cloning is already done; set is ok
+ %% if new protocol is usmNoPrivProtocol
+ case PrivProtocol of
+ ?usmNoPrivProtocol ->
+ ok;
+ ?usmDESPrivProtocol ->
+ inconsistentValue(?usmUserPrivProtocol);
+ ?usmAesCfb128Protocol ->
+ inconsistentValue(?usmUserPrivProtocol);
+ _ ->
+ wrongValue(?usmUserPrivProtocol)
+ end;
+ _ ->
+ %% Otherwise, check that the new protocol is known,
+ %% and that the system we're running supports the
+ %% crypto function.
+ case PrivProtocol of
+ ?usmNoPrivProtocol ->
+ ok;
+ ?usmDESPrivProtocol ->
+ %% The 'catch' handles the case when 'crypto' is
+ %% not present in the system.
+ case is_crypto_supported(des_cbc_decrypt) of
+ true ->
+ case get_auth_proto(RowIndex, Cols) of
+ ?usmNoAuthProtocol ->
+ inconsistentValue(?usmUserPrivProtocol);
+ _ ->
+ ok
+ end;
+ false ->
+ wrongValue(?usmUserPrivProtocol)
+ end;
+ ?usmAesCfb128Protocol ->
+ %% The 'catch' handles the case when 'crypto' is
+ %% not present in the system.
+ case is_crypto_supported(aes_cfb_128_decrypt) of
+ true ->
+ case get_auth_proto(RowIndex, Cols) of
+ ?usmNoAuthProtocol ->
+ inconsistentValue(?usmUserPrivProtocol);
+ _ ->
+ ok
+ end;
+ false ->
+ wrongValue(?usmUserPrivProtocol)
+ end;
+ _ -> wrongValue(?usmUserPrivProtocol)
+ end
+ end;
+ false ->
+ ok
+ end.
+
+
+set_clone_from(RowIndex, Cols) ->
+ %% If CloneFrom is modified, do the cloning.
+ case lists:keysearch(?usmUserCloneFrom, 1, Cols) of
+ {value, {_Col, RowPointer}} ->
+ RowIndex2 = extract_row(RowPointer), % won't fail
+ CloneRow = snmp_generic:table_get_row(db(usmUserTable), RowIndex2,
+ foi(usmUserTable)),
+ AuthP = element(?usmUserAuthProtocol, CloneRow),
+ PrivP = element(?usmUserPrivProtocol, CloneRow),
+ AuthK = element(?usmUserAuthKey, CloneRow),
+ PrivK = element(?usmUserPrivKey, CloneRow),
+ SCols = [{?usmUserAuthProtocol, AuthP},
+ {?usmUserPrivProtocol, PrivP},
+ {?usmUserAuthKey, AuthK},
+ {?usmUserPrivKey, PrivK}],
+ case snmp_generic:table_set_elements(db(usmUserTable),
+ RowIndex,
+ SCols) of
+ true -> ok;
+ false -> {commitFailed, ?usmUserCloneFrom}
+ end;
+ false ->
+ ok
+ end.
+
+set_auth_key_change(RowIndex, Cols) ->
+ set_key_change(RowIndex, Cols, ?usmUserAuthKeyChange, auth).
+
+set_own_auth_key_change(RowIndex, Cols) ->
+ set_key_change(RowIndex, Cols, ?usmUserOwnAuthKeyChange, auth).
+
+set_priv_key_change(RowIndex, Cols) ->
+ set_key_change(RowIndex, Cols, ?usmUserPrivKeyChange, priv).
+
+set_own_priv_key_change(RowIndex, Cols) ->
+ set_key_change(RowIndex, Cols, ?usmUserOwnPrivKeyChange, priv).
+
+set_key_change(RowIndex, Cols, KeyChangeCol, Type) ->
+ case lists:keysearch(KeyChangeCol, 1, Cols) of
+ {value, {_Col, KeyChange}} ->
+ KeyCol = case Type of
+ auth -> ?usmUserAuthKey;
+ priv -> ?usmUserPrivKey
+ end,
+ [AuthP, Key] =
+ snmp_generic:table_get_elements(db(usmUserTable),
+ RowIndex,
+ [?usmUserAuthProtocol,
+ KeyCol]),
+ NewKey = extract_new_key(AuthP, Key, KeyChange),
+ snmp_generic:table_set_element(db(usmUserTable), RowIndex,
+ KeyCol, NewKey);
+ false ->
+ ok
+ end.
+
+%% Extract the UserName part from a RowIndex.
+get_user_name([L1 | Rest]) -> get_user_name(L1, Rest).
+get_user_name(0, [_L2 | UserName]) -> UserName;
+get_user_name(N, [_H | T]) -> get_user_name(N-1, T).
+
+extract_row(RowPtr) -> extract_row(?usmUserEntry, RowPtr).
+extract_row([H | T], [H | T2]) -> extract_row(T, T2);
+extract_row([], [?usmUserSecurityName | T]) -> T;
+extract_row(_, _) -> wrongValue(?usmUserCloneFrom).
+
+%% Pre: the user exixt
+get_auth_proto(RowIndex, Cols) ->
+ %% The protocol can be chanegd by the request too, otherwise,
+ %% check the stored protocol.
+ case lists:keysearch(?usmUserAuthProtocol, 1, Cols) of
+ {value, {_, Protocol}} ->
+ Protocol;
+ false ->
+ %% OTP-3596
+ case snmp_generic:table_get_element(db(usmUserTable),
+ RowIndex,
+ ?usmUserAuthProtocol) of
+ {value, Protocol} ->
+ Protocol;
+ _ ->
+ undefined
+ end
+ end.
+
+%% Pre: the user exixt
+get_priv_proto(RowIndex, Cols) ->
+ %% The protocol can be chanegd by the request too, otherwise,
+ %% check the stored protocol.
+ case lists:keysearch(?usmUserPrivProtocol, 1, Cols) of
+ {value, {_, Protocol}} ->
+ Protocol;
+ false ->
+ %% OTP-3596
+ case snmp_generic:table_get_element(db(usmUserTable),
+ RowIndex,
+ ?usmUserPrivProtocol) of
+ {value, Protocol} ->
+ Protocol;
+ _ ->
+ undefined
+ end
+ end.
+
+
+db(X) -> snmpa_agent:db(X).
+
+fa(usmUserTable) -> ?usmUserSecurityName.
+
+foi(usmUserTable) -> ?usmUserEngineID.
+
+noc(usmUserTable) -> 13.
+
+stc(usmUserTable) -> ?usmUserStorageType.
+
+next(Name, RowIndex, Cols) ->
+ snmp_generic:handle_table_next(db(Name), RowIndex, Cols,
+ fa(Name), foi(Name), noc(Name)).
+
+table_next(Name, RestOid) ->
+ snmp_generic:table_next(db(Name), RestOid).
+
+
+get(Name, RowIndex, Cols) ->
+ snmp_generic:handle_table_get(db(Name), RowIndex, Cols, foi(Name)).
+
+%%-----------------------------------------------------------------
+%% Key change functions. The KeyChange Texual-Convention is
+%% defined in the SNMP-USER-BASED-SM-MIB.
+%% Note that this implementation supports md5 and sha, which
+%% both have fixed length requirements on the length of the key;
+%% thus the implementation can be (and is) simplified.
+%%-----------------------------------------------------------------
+mk_key_change(Hash, OldKey, NewKey) ->
+ KeyLen = length(NewKey),
+ Alg = case Hash of
+ ?usmHMACMD5AuthProtocol -> md5;
+ ?usmHMACSHAAuthProtocol -> sha;
+ md5 -> md5;
+ sha -> sha
+ end,
+ Random = mk_random(KeyLen),
+ mk_key_change(Alg, OldKey, NewKey, KeyLen, Random).
+
+%% This function is only exported for test purposes. There is a test
+%% case in the standard where Random is pre-defined.
+mk_key_change(Alg, OldKey, NewKey, KeyLen, Random) ->
+ %% OldKey and Random is of length KeyLen...
+ Digest = lists:sublist(binary_to_list(crypto:Alg(OldKey++Random)), KeyLen),
+ %% ... and so is Digest
+ Delta = snmp_misc:str_xor(Digest, NewKey),
+ Random ++ Delta.
+
+%% Extracts a new Key from a KeyChange value, sent by a manager.
+extract_new_key(?usmNoAuthProtocol, OldKey, _KeyChange) ->
+ OldKey;
+extract_new_key(Hash, OldKey, KeyChange) ->
+ KeyLen = length(OldKey),
+ Alg = case Hash of
+ ?usmHMACMD5AuthProtocol -> md5;
+ ?usmHMACSHAAuthProtocol -> sha;
+ md5 -> md5;
+ sha -> sha
+ end,
+ {Random, Delta} = split(KeyLen, KeyChange, []),
+ Digest = lists:sublist(binary_to_list(crypto:Alg(OldKey++Random)), KeyLen),
+ NewKey = snmp_misc:str_xor(Digest, Delta),
+ NewKey.
+
+-define(i16(Int), (Int bsr 8) band 255, Int band 255).
+-define(i8(Int), Int band 255).
+
+mk_random(Len) when Len =< 20 ->
+ %% Use of yield():
+ %% This will either schedule another process, or fail and invoke
+ %% the error_handler (in old versions). In either case, it is
+ %% safe to assume that now, reductions and garbage_collection have
+ %% changed in a non-deterministically way.
+ {_,_,A} = erlang:now(),
+ catch erlang:yield(),
+ {_,_,B} = erlang:now(),
+ catch erlang:yield(),
+ {_,_,C} = erlang:now(),
+ {D,_} = erlang:statistics(reductions),
+ {E,_} = erlang:statistics(runtime),
+ {F,_} = erlang:statistics(wall_clock),
+ {G,H,_} = erlang:statistics(garbage_collection),
+ catch erlang:yield(),
+ {_,_,C2} = erlang:now(),
+ {D2,_} = erlang:statistics(reductions),
+ {_,H2,_} = erlang:statistics(garbage_collection),
+ %% X(N) means we can use N bits from variable X:
+ %% A(16) B(16) C(16) D(16) E(8) F(16) G(8) H(16)
+ Rnd20 = [?i16(A),?i16(B),?i16(C),?i16(D),?i8(E),?i16(F),
+ ?i8(G),?i16(H),?i16(C2),?i16(D2),?i16(H2)],
+ lists:sublist(Rnd20, Len).
+
+split(0, Rest, FirstRev) ->
+ {lists:reverse(FirstRev), Rest};
+split(N, [H | T], FirstRev) when N > 0 ->
+ split(N-1, T, [H | FirstRev]).
+
+
+is_crypto_supported(Func) ->
+ %% The 'catch' handles the case when 'crypto' is
+ %% not present in the system (or not started).
+ case catch lists:member(Func, crypto:info()) of
+ true -> true;
+ _ -> false
+ end.
+
+inconsistentValue(V) -> throw({inconsistentValue, V}).
+inconsistentName(N) -> throw({inconsistentName, N}).
+wrongValue(V) -> throw({wrongValue, V}).
+noAccess(C) -> throw({noAccess, C}).
+
+set_sname() ->
+ set_sname(get(sname)).
+
+set_sname(undefined) ->
+ put(sname,conf);
+set_sname(_) -> %% Keep it, if already set.
+ ok.
+
+error(Reason) ->
+ throw({error, Reason}).
+
+%%-----------------------------------------------------------------
+
+info_msg(F, A) ->
+ ?snmpa_info("USM: " ++ F, A).
+
+%% ---
+
+config_err(F, A) ->
+ snmpa_error:config_err("[USER-BASED-SM-MIB]: " ++ F, A).
+