2 # -*- encoding: utf-8; py-indent-offset: 4 -*-
3 # +------------------------------------------------------------------+
4 # | ____ _ _ __ __ _ __ |
5 # | / ___| |__ ___ ___| | __ | \/ | |/ / |
6 # | | | | '_ \ / _ \/ __| |/ / | |\/| | ' / |
7 # | | |___| | | | __/ (__| < | | | | . \ |
8 # | \____|_| |_|\___|\___|_|\_\___|_| |_|_|\_\ |
10 # | Copyright Mathias Kettner 2014 mk@mathias-kettner.de |
11 # +------------------------------------------------------------------+
13 # This file is part of Check_MK.
14 # The official homepage is at http://mathias-kettner.de/check_mk.
16 # check_mk is free software; you can redistribute it and/or modify it
17 # under the terms of the GNU General Public License as published by
18 # the Free Software Foundation in version 2. check_mk is distributed
19 # in the hope that it will be useful, but WITHOUT ANY WARRANTY; with-
20 # out even the implied warranty of MERCHANTABILITY or FITNESS FOR A
21 # PARTICULAR PURPOSE. See the GNU General Public License for more de-
22 # tails. You should have received a copy of the GNU General Public
23 # License along with GNU Make; see the file COPYING. If not, write
24 # to the Free Software Foundation, Inc., 51 Franklin St, Fifth Floor,
25 # Boston, MA 02110-1301 USA.
27 # TODO: Rework connection management and multiplexing
34 import pathlib2
as pathlib
38 import cmk
.utils
.paths
39 import cmk
.utils
.store
as store
42 import cmk
.gui
.utils
as utils
43 import cmk
.gui
.config
as config
44 import cmk
.gui
.hooks
as hooks
45 import cmk
.gui
.background_job
as background_job
46 import cmk
.gui
.gui_background_job
as gui_background_job
47 from cmk
.gui
.exceptions
import MKUserError
, MKInternalError
48 from cmk
.gui
.log
import logger
49 from cmk
.gui
.valuespec
import (
55 from cmk
.gui
.i18n
import _
56 from cmk
.gui
.globals import html
57 import cmk
.gui
.plugins
.userdb
58 from cmk
.gui
.plugins
.userdb
.htpasswd
import Htpasswd
60 from cmk
.gui
.plugins
.userdb
.utils
import (
61 user_attribute_registry
,
62 user_connector_registry
,
65 # Datastructures and functions needed before plugins can be loaded
66 loaded_with_language
= False
68 # Custom user attributes
70 builtin_user_attribute_names
= []
72 # Connection configuration
74 # Connection object dictionary
76 auth_logger
= logger
.getChild("auth")
79 # Load all userdb plugins
80 def load_plugins(force
):
81 update_config_based_user_attributes()
83 connection_dict
.clear()
84 for connection
in config
.user_connections
:
85 connection_dict
[connection
['id']] = connection
87 # Cleanup eventual still open connections
91 global loaded_with_language
92 if loaded_with_language
== cmk
.gui
.i18n
.get_current_language() and not force
:
95 utils
.load_web_plugins("userdb", globals())
97 # This must be set after plugin loading to make broken plugins raise
98 # exceptions all the time and not only the first time (when the plugins
100 loaded_with_language
= cmk
.gui
.i18n
.get_current_language()
103 # Cleans up at the end of a request: Cleanup eventual open connections
106 g_connections
.clear()
109 # Returns a list of two part tuples where the first element is the unique
110 # connection id and the second element the connector specification dict
111 def get_connections(only_enabled
=False):
113 for connector_id
, connector_class
in user_connector_registry
.items():
114 if connector_id
== 'htpasswd':
115 # htpasswd connector is enabled by default and always executed first
116 connections
.insert(0, ('htpasswd', connector_class({})))
118 for connection_config
in config
.user_connections
:
119 if only_enabled
and connection_config
.get('disabled'):
122 connection
= connector_class(connection_config
)
124 if only_enabled
and not connection
.is_enabled():
127 connections
.append((connection_config
['id'], connection
))
131 def active_connections():
132 return get_connections(only_enabled
=True)
135 def connection_choices():
136 return sorted([(cid
, "%s (%s)" % (cid
, c
.type()))
137 for cid
, c
in get_connections(only_enabled
=False)
138 if c
.type() == "ldap"],
139 key
=lambda x_y
: x_y
[1])
142 # When at least one LDAP connection is defined and active a sync is possible
144 return any(connection
.type() == "ldap" for _connection_id
, connection
in active_connections())
147 def cleanup_connection_id(connection_id
):
148 if connection_id
is None:
149 connection_id
= 'htpasswd'
151 # Old Check_MK used a static "ldap" connector id for all LDAP users.
152 # Since Check_MK now supports multiple LDAP connections, the ID has
153 # been changed to "default". But only transform this when there is
154 # no connection existing with the id LDAP.
155 if connection_id
== 'ldap' and not get_connection('ldap'):
156 connection_id
= 'default'
161 # Returns the connection object of the requested connection id. This function
162 # maintains a cache that for a single connection_id only one object per request
164 def get_connection(connection_id
):
165 if connection_id
in g_connections
:
166 return g_connections
[connection_id
]
168 connection
= dict(get_connections()).get(connection_id
)
171 g_connections
[connection_id
] = connection
176 # Returns a list of connection specific locked attributes
177 def locked_attributes(connection_id
):
178 return get_attributes(connection_id
, "locked_attributes")
181 # Returns a list of connection specific multisite attributes
182 def multisite_attributes(connection_id
):
183 return get_attributes(connection_id
, "multisite_attributes")
186 # Returns a list of connection specific non contact attributes
187 def non_contact_attributes(connection_id
):
188 return get_attributes(connection_id
, "non_contact_attributes")
191 def get_attributes(connection_id
, what
):
192 connection
= get_connection(connection_id
)
194 return getattr(connection
, what
)()
198 def new_user_template(connection_id
):
201 'connector': connection_id
,
204 # Apply the default user profile
205 new_user
.update(config
.default_user_profile
)
209 def create_non_existing_user(connection_id
, username
):
210 if user_exists(username
):
211 return # User exists. Nothing to do...
213 users
= load_users(lock
=True)
214 users
[username
] = new_user_template(connection_id
)
217 # Call the sync function for this new user
218 connection
= get_connection(connection_id
)
220 connection
.do_sync(add_to_changelog
=False, only_username
=username
)
221 except cmk
.gui
.plugins
.userdb
.ldap_connector
.MKLDAPException
as e
:
222 show_exception(connection_id
, _("Error during sync"), e
, debug
=config
.debug
)
223 except Exception as e
:
224 show_exception(connection_id
, _("Error during sync"), e
)
227 def is_customer_user_allowed_to_login(user_id
):
228 if not cmk
.is_managed_edition():
231 import cmk
.gui
.cme
.managed
as managed
232 user
= config
.LoggedInUser(user_id
)
233 customer_id
= managed
.get_customer_id(user
.attributes
)
235 if managed
.is_global(customer_id
):
238 return managed
.is_current_customer(customer_id
)
241 # This function is called very often during regular page loads so it has to be efficient
242 # even when having a lot of users.
244 # When using the multisite authentication with just by WATO created users it would be
245 # easy, but we also need to deal with users which are only existant in the htpasswd
246 # file and don't have a profile directory yet.
247 def user_exists(username
):
248 if _user_exists_according_to_profile(username
):
251 return Htpasswd(pathlib
.Path(cmk
.utils
.paths
.htpasswd_file
)).exists(username
)
254 def _user_exists_according_to_profile(username
):
255 base_path
= config
.config_dir
+ "/" + username
.encode("utf-8") + "/"
256 return os
.path
.exists(base_path
+ "transids.mk") \
257 or os
.path
.exists(base_path
+ "serial.mk")
260 def user_locked(username
):
262 return users
[username
].get('locked', False)
265 def login_timed_out(username
, last_activity
):
266 idle_timeout
= load_custom_attr(username
, "idle_timeout", convert_idle_timeout
, None)
267 if idle_timeout
is None:
268 idle_timeout
= config
.user_idle_timeout
270 if idle_timeout
in [None, False]:
271 return False # no timeout activated at all
273 timed_out
= (time
.time() - last_activity
) > idle_timeout
276 auth_logger
.debug("%s login timed out (Inactive for %d seconds)" %
277 (username
, time
.time() - last_activity
))
282 def update_user_access_time(username
):
283 if not config
.save_user_access_times
:
285 save_custom_attr(username
, 'last_seen', repr(time
.time()))
288 def on_succeeded_login(username
):
289 num_failed_logins
= load_custom_attr(username
, 'num_failed_logins', utils
.saveint
)
290 if num_failed_logins
is not None and num_failed_logins
!= 0:
291 save_custom_attr(username
, 'num_failed_logins', '0')
293 update_user_access_time(username
)
296 # userdb.need_to_change_pw returns either False or the reason description why the
297 # password needs to be changed
298 def need_to_change_pw(username
):
299 if load_custom_attr(username
, 'enforce_pw_change', utils
.saveint
) == 1:
302 last_pw_change
= load_custom_attr(username
, 'last_pw_change', utils
.saveint
)
303 max_pw_age
= config
.password_policy
.get('max_age')
305 if not last_pw_change
:
306 # The age of the password is unknown. Assume the user has just set
307 # the password to have the first access after enabling password aging
308 # as starting point for the password period. This bewares all users
309 # from needing to set a new password after enabling aging.
310 save_custom_attr(username
, 'last_pw_change', str(int(time
.time())))
312 elif time
.time() - last_pw_change
> max_pw_age
:
317 def on_failed_login(username
):
318 users
= load_users(lock
=True)
319 if username
in users
:
320 if "num_failed_logins" in users
[username
]:
321 users
[username
]["num_failed_logins"] += 1
323 users
[username
]["num_failed_logins"] = 1
325 if config
.lock_on_logon_failures
:
326 if users
[username
]["num_failed_logins"] >= config
.lock_on_logon_failures
:
327 users
[username
]["locked"] = True
332 root_dir
= cmk
.utils
.paths
.check_mk_config_dir
+ "/wato/"
333 multisite_dir
= cmk
.utils
.paths
.default_config_dir
+ "/multisite.d/wato/"
338 # title = _('Automatic User Synchronization'),
339 # help = _('By default the users are synchronized automatically in several situations. '
340 # 'The sync is started when opening the "Users" page in configuration and '
341 # 'during each page rendering. Each connector can then specify if it wants to perform '
342 # 'any actions. For example the LDAP connector will start the sync once the cached user '
343 # 'information are too old.'),
344 # default_value = [ 'wato_users', 'page', 'wato_pre_activate_changes', 'wato_snapshot_pushed' ],
346 # ('page', _('During regular page processing')),
347 # ('wato_users', _('When opening the users\' configuration page')),
348 # ('wato_pre_activate_changes', _('Before activating the changed configuration')),
349 # ('wato_snapshot_pushed', _('On a remote site, when it receives a new configuration')),
351 # allow_empty = True,
353 def transform_userdb_automatic_sync(val
):
355 # legacy compat - disabled
358 elif isinstance(val
, list) and val
:
359 # legacy compat - all connections
365 class UserSelection(DropdownChoice
):
366 """Dropdown for choosing a multisite user"""
368 def __init__(self
, **kwargs
):
369 only_contacts
= kwargs
.get("only_contacts", False)
370 kwargs
["choices"] = self
._generate
_wato
_users
_elements
_function
(
371 kwargs
.get("none"), only_contacts
=only_contacts
)
372 kwargs
["invalid_choice"] = "complain" # handle vanished users correctly!
373 DropdownChoice
.__init
__(self
, **kwargs
)
375 def _generate_wato_users_elements_function(self
, none_value
, only_contacts
=False):
376 def get_wato_users(nv
):
378 elements
= [(name
, "%s - %s" % (name
, us
.get("alias", name
)))
379 for (name
, us
) in users
.items()
380 if (not only_contacts
or us
.get("contactgroups"))]
383 elements
= [(None, none_value
)] + elements
386 return lambda: get_wato_users(none_value
)
388 def value_to_text(self
, value
):
389 text
= DropdownChoice
.value_to_text(self
, value
)
390 return text
.split(" - ")[-1]
394 # .--User Session--------------------------------------------------------.
396 # | | | | |___ ___ _ __ / ___| ___ ___ ___(_) ___ _ __ |
397 # | | | | / __|/ _ \ '__| \___ \ / _ \/ __/ __| |/ _ \| '_ \ |
398 # | | |_| \__ \ __/ | ___) | __/\__ \__ \ | (_) | | | | |
399 # | \___/|___/\___|_| |____/ \___||___/___/_|\___/|_| |_| |
401 # +----------------------------------------------------------------------+
402 # | When single users sessions are activated, a user an only login once |
403 # | a time. In case a user tries to login a second time, an error is |
404 # | shown to the later login. |
406 # | To make this feature possible a session ID is computed during login, |
407 # | saved in the users cookie and stored in the user profile together |
408 # | with the current time as "last activity" timestamp. This timestamp |
409 # | is updated during each user activity in the GUI. |
411 # | Once a user logs out or the "last activity" is older than the |
412 # | configured session timeout, the session is invalidated. The user |
413 # | can then login again from the same client or another one. |
414 # '----------------------------------------------------------------------'
417 def is_valid_user_session(username
, session_id
):
418 if config
.single_user_session
is None:
419 return True # No login session limitation enabled, no validation
421 session_info
= load_session_info(username
)
422 if session_info
is None:
423 return False # no session active
425 active_session_id
, last_activity
= session_info
427 if session_id
== active_session_id
:
428 return True # Current session. Fine.
430 auth_logger
.debug("%s session_id not valid (timed out?) (Inactive for %d seconds)" %
431 (username
, time
.time() - last_activity
))
436 def ensure_user_can_init_session(username
):
437 if config
.single_user_session
is None:
438 return True # No login session limitation enabled, no validation
440 session_timeout
= config
.single_user_session
442 session_info
= load_session_info(username
)
443 if session_info
is None:
444 return True # No session active
446 last_activity
= session_info
[1]
447 if (time
.time() - last_activity
) > session_timeout
:
448 return True # Former active session timed out
450 auth_logger
.debug("%s another session is active (inactive for: %d seconds)" %
451 (username
, time
.time() - last_activity
))
453 raise MKUserError(None, _("Another session is active"))
456 # Creates a new user login session (if single user session mode is enabled) and
457 # returns the session_id of the new session.
458 def initialize_session(username
):
459 if not config
.single_user_session
:
462 session_id
= create_session_id()
463 save_session_info(username
, session_id
)
467 # Creates a random session id for the user and returns it.
468 def create_session_id():
469 return utils
.gen_id()
472 # Updates the current session of the user and returns the session_id or only
473 # returns an empty string when single user session mode is not enabled.
474 def refresh_session(username
):
475 if not config
.single_user_session
:
478 session_info
= load_session_info(username
)
479 if session_info
is None:
480 return # Don't refresh. Session is not valid anymore
482 session_id
= session_info
[0]
483 save_session_info(username
, session_id
)
486 def invalidate_session(username
):
487 remove_custom_attr(username
, "session_info")
490 # Saves the current session_id and the current time (last activity)
491 def save_session_info(username
, session_id
):
492 save_custom_attr(username
, "session_info", "%s|%s" % (session_id
, int(time
.time())))
495 # Returns either None (when no session_id available) or a two element
496 # tuple where the first element is the sesssion_id and the second the
497 # timestamp of the last activity.
498 def load_session_info(username
):
499 return load_custom_attr(username
, "session_info", convert_session_info
)
502 def convert_session_info(value
):
506 session_id
, last_activity
= value
.split("|", 1)
507 return session_id
, int(last_activity
)
511 # .-Users----------------------------------------------------------------.
513 # | | | | |___ ___ _ __ ___ |
514 # | | | | / __|/ _ \ '__/ __| |
515 # | | |_| \__ \ __/ | \__ \ |
516 # | \___/|___/\___|_| |___/ |
518 # +----------------------------------------------------------------------+
521 class GenericUserAttribute(cmk
.gui
.plugins
.userdb
.UserAttribute
):
522 def __init__(self
, user_editable
, show_in_table
, add_custom_macro
, domain
, permission
,
524 super(GenericUserAttribute
, self
).__init
__()
525 self
._user
_editable
= user_editable
526 self
._show
_in
_table
= show_in_table
527 self
._add
_custom
_macro
= add_custom_macro
528 self
._domain
= domain
529 self
._permission
= permission
530 self
._from
_config
= from_config
532 def from_config(self
):
533 return self
._from
_config
535 def user_editable(self
):
536 return self
._user
_editable
538 def permission(self
):
539 return self
._permission
541 def show_in_table(self
):
542 return self
._show
_in
_table
544 def add_custom_macro(self
):
545 return self
._add
_custom
_macro
551 # TODO: Legacy plugin API. Converts to new internal structure. Drop this with 1.6 or later.
552 def declare_user_attribute(name
,
558 add_custom_macro
=False,
562 # FIXME: The classmethods "name" and "topic" shadow the arguments from the function scope.
563 # Any use off "name" and "topic" inside the class will result in a NameError.
567 @user_attribute_registry.register
568 class LegacyUserAttribute(GenericUserAttribute
):
571 _topic
= attr_topic
if attr_topic
else 'personal'
579 return cls
._valuespec
586 super(LegacyUserAttribute
, self
).__init
__(
587 user_editable
=user_editable
,
588 show_in_table
=show_in_table
,
589 add_custom_macro
=add_custom_macro
,
591 permission
=permission
,
592 from_config
=from_config
,
596 def get_user_attributes():
597 return [(k
, v()) for k
, v
in user_attribute_registry
.items()]
600 def load_users(lock
=False):
601 filename
= root_dir
+ "contacts.mk"
604 # Note: the lock will be released on next save_users() call or at
605 # end of page request automatically.
606 store
.aquire_lock(filename
)
608 if html
.is_cached('users'):
609 return html
.get_cached('users')
611 # First load monitoring contacts from Check_MK's world. If this is
612 # the first time, then the file will be empty, which is no problem.
613 # Execfile will the simply leave contacts = {} unchanged.
614 contacts
= store
.load_from_mk_file(filename
, "contacts", {})
616 # Now load information about users from the GUI config world
617 filename
= multisite_dir
+ "users.mk"
618 users
= store
.load_from_mk_file(multisite_dir
+ "users.mk", "multisite_users", {})
620 # Merge them together. Monitoring users not known to Multisite
621 # will be added later as normal users.
623 for uid
, user
in users
.items():
624 # Transform user IDs which were stored with a wrong type
625 if isinstance(uid
, str):
626 uid
= uid
.decode("utf-8")
628 profile
= contacts
.get(uid
, {})
630 result
[uid
] = profile
632 # Convert non unicode mail addresses
633 if isinstance(profile
.get("email"), str):
634 profile
["email"] = profile
["email"].decode("utf-8")
636 # This loop is only neccessary if someone has edited
637 # contacts.mk manually. But we want to support that as
639 for uid
, contact
in contacts
.items():
640 # Transform user IDs which were stored with a wrong type
641 if isinstance(uid
, str):
642 uid
= uid
.decode("utf-8")
644 if uid
not in result
:
645 result
[uid
] = contact
646 result
[uid
]["roles"] = ["user"]
647 result
[uid
]["locked"] = True
648 result
[uid
]["password"] = ""
650 # Passwords are read directly from the apache htpasswd-file.
651 # That way heroes of the command line will still be able to
652 # change passwords with htpasswd. Users *only* appearing
653 # in htpasswd will also be loaded and assigned to the role
654 # they are getting according to the multisite old-style
655 # configuration variables.
663 # FIXME TODO: Consolidate with htpasswd user connector
664 filename
= cmk
.utils
.paths
.htpasswd_file
665 for line
in readlines(filename
):
668 uid
, password
= line
.strip().split(":")[:2]
669 uid
= uid
.decode("utf-8")
670 if password
.startswith("!"):
672 password
= password
[1:]
676 result
[uid
]["password"] = password
677 result
[uid
]["locked"] = locked
679 # Create entry if this is an admin user
681 "roles": config
.roles_of_user(uid
),
682 "password": password
,
685 result
[uid
] = new_user
686 # Make sure that the user has an alias
687 result
[uid
].setdefault("alias", uid
)
688 # Other unknown entries will silently be dropped. Sorry...
690 # Now read the serials, only process for existing users
691 serials_file
= '%s/auth.serials' % os
.path
.dirname(cmk
.utils
.paths
.htpasswd_file
)
692 for line
in readlines(serials_file
):
695 user_id
, serial
= line
.split(':')[:2]
696 user_id
= user_id
.decode("utf-8")
697 if user_id
in result
:
698 result
[user_id
]['serial'] = utils
.saveint(serial
)
700 # Now read the user specific files
701 directory
= cmk
.utils
.paths
.var_dir
+ "/web/"
702 for d
in os
.listdir(directory
):
704 uid
= d
.decode("utf-8")
706 # read special values from own files
708 for attr
, conv_func
in [
709 ('num_failed_logins', utils
.saveint
),
710 ('last_pw_change', utils
.saveint
),
711 ('last_seen', utils
.savefloat
),
712 ('enforce_pw_change', lambda x
: bool(utils
.saveint(x
))),
713 ('idle_timeout', convert_idle_timeout
),
714 ('session_id', convert_session_info
),
716 val
= load_custom_attr(uid
, attr
, conv_func
)
718 result
[uid
][attr
] = val
720 # read automation secrets and add them to existing
721 # users or create new users automatically
723 secret
= file(directory
+ d
+ "/automation.secret").read().strip()
728 result
[uid
]["automation_secret"] = secret
732 "automation_secret": secret
,
735 # populate the users cache
736 html
.set_cache('users', result
)
741 def custom_attr_path(userid
, key
):
742 return cmk
.utils
.paths
.var_dir
+ "/web/" + cmk
.utils
.make_utf8(userid
) + "/" + key
+ ".mk"
745 def load_custom_attr(userid
, key
, conv_func
, default
=None):
746 path
= custom_attr_path(userid
, key
)
748 return conv_func(file(path
).read().strip())
753 def save_custom_attr(userid
, key
, val
):
754 path
= custom_attr_path(userid
, key
)
755 store
.mkdir(os
.path
.dirname(path
))
756 store
.save_file(path
, '%s\n' % val
)
759 def remove_custom_attr(userid
, key
):
761 os
.unlink(custom_attr_path(userid
, key
))
763 pass # Ignore non existing files
766 def get_online_user_ids():
767 online_threshold
= time
.time() - config
.user_online_maxage
769 for user_id
, user
in load_users(lock
=False).items():
770 if user
.get('last_seen', 0) >= online_threshold
:
771 users
.append(user_id
)
775 def split_dict(d
, keylist
, positive
):
776 return dict([(k
, v
) for (k
, v
) in d
.items() if (k
in keylist
) == positive
])
779 def save_users(profiles
):
780 write_contacts_and_users_file(profiles
)
782 # Execute user connector save hooks
785 updated_profiles
= _add_custom_macro_attributes(profiles
)
787 _save_auth_serials(updated_profiles
)
788 _save_user_profiles(updated_profiles
)
789 _cleanup_old_user_profiles(updated_profiles
)
791 # Release the lock to make other threads access possible again asap
792 # This lock is set by load_users() only in the case something is expected
793 # to be written (like during user syncs, wato, ...)
796 # populate the users cache
797 # TODO: Can we clean this up?
798 html
.set_cache('users', updated_profiles
)
800 # Call the users_saved hook
801 hooks
.call("users-saved", updated_profiles
)
804 def release_users_lock():
805 store
.release_lock(root_dir
+ "contacts.mk")
808 # TODO: Isn't this needed only while generating the contacts.mk?
809 # Check this and move it to the right place
810 def _add_custom_macro_attributes(profiles
):
811 updated_profiles
= copy
.deepcopy(profiles
)
814 core_custom_macros
= [k
for k
, o
in user_attributes
.items() if o
.get('add_custom_macro')]
815 for user
in updated_profiles
.keys():
816 for macro
in core_custom_macros
:
817 if macro
in updated_profiles
[user
]:
818 updated_profiles
[user
]['_' + macro
] = updated_profiles
[user
][macro
]
820 return updated_profiles
823 # Write user specific files
824 def _save_user_profiles(updated_profiles
):
825 non_contact_keys
= _non_contact_keys()
826 multisite_keys
= _multisite_keys()
828 for user_id
, user
in updated_profiles
.items():
829 user_dir
= cmk
.utils
.paths
.var_dir
+ "/web/" + user_id
.encode("utf-8")
830 store
.mkdir(user_dir
)
832 # authentication secret for local processes
833 auth_file
= user_dir
+ "/automation.secret"
834 if "automation_secret" in user
:
835 store
.save_file(auth_file
, "%s\n" % user
["automation_secret"])
836 elif os
.path
.exists(auth_file
):
839 # Write out user attributes which are written to dedicated files in the user
840 # profile directory. The primary reason to have separate files, is to reduce
841 # the amount of data to be loaded during regular page processing
842 save_custom_attr(user_id
, 'serial', str(user
.get('serial', 0)))
843 save_custom_attr(user_id
, 'num_failed_logins', str(user
.get('num_failed_logins', 0)))
844 save_custom_attr(user_id
, 'enforce_pw_change', str(
845 int(user
.get('enforce_pw_change', False))))
846 save_custom_attr(user_id
, 'last_pw_change', str(
847 user
.get('last_pw_change', int(time
.time()))))
849 if "idle_timeout" in user
:
850 save_custom_attr(user_id
, "idle_timeout", user
["idle_timeout"])
852 remove_custom_attr(user_id
, "idle_timeout")
854 # Write out the last seent time
855 if 'last_seen' in user
:
856 save_custom_attr(user_id
, 'last_seen', repr(user
['last_seen']))
858 save_cached_profile(user_id
, user
, multisite_keys
, non_contact_keys
)
861 # During deletion of users we don't delete files which might contain user settings
862 # and e.g. customized views which are not easy to reproduce. We want to keep the
863 # files which are the result of a lot of work even when e.g. the LDAP sync deletes
864 # a user by accident. But for some internal files it is ok to delete them.
866 # Be aware: The user_exists() function relies on these files to be deleted.
867 def _cleanup_old_user_profiles(updated_profiles
):
868 profile_files_to_delete
= [
873 directory
= cmk
.utils
.paths
.var_dir
+ "/web"
874 for user_dir
in os
.listdir(cmk
.utils
.paths
.var_dir
+ "/web"):
875 if user_dir
not in ['.', '..'] and user_dir
.decode("utf-8") not in updated_profiles
:
876 entry
= directory
+ "/" + user_dir
877 if not os
.path
.isdir(entry
):
880 for to_delete
in profile_files_to_delete
:
881 if os
.path
.exists(entry
+ '/' + to_delete
):
882 os
.unlink(entry
+ '/' + to_delete
)
885 def write_contacts_and_users_file(profiles
, custom_default_config_dir
=None):
886 non_contact_keys
= _non_contact_keys()
887 multisite_keys
= _multisite_keys()
888 updated_profiles
= _add_custom_macro_attributes(profiles
)
890 if custom_default_config_dir
:
891 check_mk_config_dir
= "%s/conf.d/wato" % custom_default_config_dir
892 multisite_config_dir
= "%s/multisite.d/wato" % custom_default_config_dir
894 check_mk_config_dir
= "%s/conf.d/wato" % cmk
.utils
.paths
.default_config_dir
895 multisite_config_dir
= "%s/multisite.d/wato" % cmk
.utils
.paths
.default_config_dir
897 non_contact_attributes_cache
= {}
898 multisite_attributes_cache
= {}
899 for user_settings
in updated_profiles
.itervalues():
900 connector
= user_settings
.get("connector")
901 if connector
not in non_contact_attributes_cache
:
902 non_contact_attributes_cache
[connector
] = non_contact_attributes(
903 user_settings
.get('connector'))
904 if connector
not in multisite_attributes_cache
:
905 multisite_attributes_cache
[connector
] = multisite_attributes(
906 user_settings
.get('connector'))
908 # Remove multisite keys in contacts.
909 # TODO: Clean this up. Just improved the performance, but still have no idea what its actually doing...
914 non_contact_keys
+ non_contact_attributes_cache
[user
.get('connector')],
916 )) for (id, user
) in updated_profiles
.items()])
918 # Only allow explicitely defined attributes to be written to multisite config
920 for uid
, profile
in updated_profiles
.items():
923 for p
, val
in profile
.items()
924 if p
in multisite_keys
+ multisite_attributes_cache
[profile
.get('connector')]])
926 # Check_MK's monitoring contacts
927 store
.save_to_mk_file(
928 "%s/%s" % (check_mk_config_dir
, "contacts.mk"),
931 pprint_value
=config
.wato_pprint_config
)
933 # GUI specific user configuration
934 store
.save_to_mk_file(
935 "%s/%s" % (multisite_config_dir
, "users.mk"),
938 pprint_value
=config
.wato_pprint_config
)
941 # User attributes not to put into contact definitions for Check_MK
942 def _non_contact_keys():
956 ] + _get_multisite_custom_variable_names()
959 # User attributes to put into multisite configuration
960 def _multisite_keys():
968 ] + _get_multisite_custom_variable_names()
971 def _get_multisite_custom_variable_names():
972 return [k
for k
, v
in user_attributes
.items() if v
["domain"] == "multisite"]
975 def _save_auth_serials(updated_profiles
):
976 # Write out the users serials
978 for user_id
, user
in updated_profiles
.items():
979 serials
+= '%s:%d\n' % (cmk
.utils
.make_utf8(user_id
), user
.get('serial', 0))
980 store
.save_file('%s/auth.serials' % os
.path
.dirname(cmk
.utils
.paths
.htpasswd_file
), serials
)
984 users
= load_users(lock
=True)
988 def create_cmk_automation_user():
989 secret
= utils
.gen_id()
991 users
= load_users(lock
=True)
992 users
["automation"] = {
993 'alias': u
"Check_MK Automation - used for calling web services",
995 'automation_secret': secret
,
996 'password': cmk
.gui
.plugins
.userdb
.htpasswd
.hash_password(secret
),
1002 'notifications_enabled': False,
1008 def save_cached_profile(user_id
, user
, multisite_keys
, non_contact_keys
):
1009 # Only save contact AND multisite attributes to the profile. Not the
1010 # infos that are stored in the custom attribute files.
1012 for key
in user
.keys():
1013 if key
in multisite_keys
or key
not in non_contact_keys
:
1014 cache
[key
] = user
[key
]
1016 config
.save_user_file("cached_profile", cache
, user_id
=user_id
)
1019 def load_cached_profile():
1020 return config
.user
.load_file("cached_profile", None)
1023 def contactgroups_of_user(user_id
):
1024 user
= load_cached_profile()
1026 # No cached profile present. Load all users to get the users data
1027 user
= load_users(lock
=False).get(user_id
, {})
1029 return user
.get("contactgroups", [])
1032 def convert_idle_timeout(value
):
1033 if value
== "False":
1034 return False # Idle timeout disabled
1039 return None # Invalid value -> use global setting
1043 # .-Roles----------------------------------------------------------------.
1045 # | | _ \ ___ | | ___ ___ |
1046 # | | |_) / _ \| |/ _ \/ __| |
1047 # | | _ < (_) | | __/\__ \ |
1048 # | |_| \_\___/|_|\___||___/ |
1050 # +----------------------------------------------------------------------+
1054 roles
= store
.load_from_mk_file(
1055 multisite_dir
+ "roles.mk",
1057 default
=_get_builtin_roles(),
1060 # Make sure that "general." is prefixed to the general permissions
1061 # (due to a code change that converted "use" into "general.use", etc.
1062 # TODO: Can't we drop this? This seems to be from very early days of the GUI
1063 for role
in roles
.values():
1064 for pname
, pvalue
in role
["permissions"].items():
1065 if "." not in pname
:
1066 del role
["permissions"][pname
]
1067 role
["permissions"]["general." + pname
] = pvalue
1069 # Reflect the data in the roles dict kept in the config module needed
1070 # for instant changes in current page while saving modified roles.
1071 # Otherwise the hooks would work with old data when using helper
1072 # functions from the config module
1073 # TODO: load_roles() should not update global structures
1074 config
.roles
.update(roles
)
1079 def _get_builtin_roles():
1080 """Returns a role dictionary containing the bultin default roles"""
1081 builtin_role_names
= {
1082 "admin": _("Administrator"),
1083 "user": _("Normal monitoring user"),
1084 "guest": _("Guest user"),
1088 "alias": builtin_role_names
.get(rid
, rid
),
1089 "permissions": {}, # use default everywhere
1091 } for rid
in config
.builtin_role_ids
1096 # .-Groups---------------------------------------------------------------.
1098 # | / ___|_ __ ___ _ _ _ __ ___ |
1099 # | | | _| '__/ _ \| | | | '_ \/ __| |
1100 # | | |_| | | | (_) | |_| | |_) \__ \ |
1101 # | \____|_| \___/ \__,_| .__/|___/ |
1103 # +----------------------------------------------------------------------+
1104 # TODO: Contact groups are fine here, but service / host groups?
1107 def load_group_information():
1108 cmk_base_groups
= _load_cmk_base_groups()
1109 gui_groups
= _load_gui_groups()
1111 # Merge information from Check_MK and Multisite worlds together
1113 for what
in ["host", "service", "contact"]:
1115 for gid
, alias
in cmk_base_groups
['define_%sgroups' % what
].items():
1116 groups
[what
][gid
] = {'alias': alias
}
1118 if gid
in gui_groups
['multisite_%sgroups' % what
]:
1119 groups
[what
][gid
].update(gui_groups
['multisite_%sgroups' % what
][gid
])
1124 def _load_cmk_base_groups():
1125 """Load group information from Check_MK world"""
1127 "define_hostgroups": {},
1128 "define_servicegroups": {},
1129 "define_contactgroups": {},
1132 return store
.load_mk_file(root_dir
+ "groups.mk", default
=group_specs
)
1135 def _load_gui_groups():
1136 # Now load information from the Web world
1138 "multisite_hostgroups": {},
1139 "multisite_servicegroups": {},
1140 "multisite_contactgroups": {},
1143 return store
.load_mk_file(multisite_dir
+ "groups.mk", default
=group_specs
)
1146 class GroupChoice(DualListChoice
):
1147 def __init__(self
, what
, with_foreign_groups
=True, **kwargs
):
1148 DualListChoice
.__init
__(self
, **kwargs
)
1150 self
._choices
= lambda: self
.load_groups(with_foreign_groups
)
1152 def load_groups(self
, with_foreign_groups
):
1153 all_groups
= load_group_information()
1154 this_group
= all_groups
.get(self
.what
, {})
1155 return sorted([(k
, t
['alias'] and t
['alias'] or k
)
1156 for (k
, t
) in this_group
.items()
1157 if with_foreign_groups
or k
in config
.user
.contact_groups()],
1158 key
=lambda x
: x
[1].lower())
1162 # .-Custom-Attrs.--------------------------------------------------------.
1164 # | / ___| _ ___| |_ ___ _ __ ___ / \ | |_| |_ _ __ ___ |
1165 # | | | | | | / __| __/ _ \| '_ ` _ \ _____ / _ \| __| __| '__/ __| |
1166 # | | |__| |_| \__ \ || (_) | | | | | |_____/ ___ \ |_| |_| | \__ \_ |
1167 # | \____\__,_|___/\__\___/|_| |_| |_| /_/ \_\__|\__|_| |___(_) |
1169 # +----------------------------------------------------------------------+
1170 # | Mange custom attributes of users (in future hosts etc.) |
1171 # '----------------------------------------------------------------------'
1174 def update_config_based_user_attributes():
1175 _clear_config_based_user_attributes()
1177 for attr
in config
.wato_user_attrs
:
1178 if attr
["type"] == "TextAscii":
1179 vs
= TextAscii(title
=attr
['title'], help=attr
['help'])
1181 raise NotImplementedError()
1183 # TODO: This method uses LegacyUserAttribute(). Use another class for
1184 # this kind of attribute
1185 declare_user_attribute(
1188 user_editable
=attr
['user_editable'],
1189 show_in_table
=attr
.get('show_in_table', False),
1190 topic
=attr
.get('topic', 'personal'),
1191 add_custom_macro
=attr
.get('add_custom_macro', False),
1196 def _clear_config_based_user_attributes():
1197 for attr_class
in user_attribute_registry
.values():
1199 if attr
.from_config():
1200 del user_attribute_registry
[attr
.name()]
1204 # .--ConnectorCfg--------------------------------------------------------.
1205 # | ____ _ ____ __ |
1206 # | / ___|___ _ __ _ __ ___ ___| |_ ___ _ __ / ___|/ _| __ _ |
1207 # | | | / _ \| '_ \| '_ \ / _ \/ __| __/ _ \| '__| | | |_ / _` | |
1208 # | | |__| (_) | | | | | | | __/ (__| || (_) | | | |___| _| (_| | |
1209 # | \____\___/|_| |_|_| |_|\___|\___|\__\___/|_| \____|_| \__, | |
1211 # +----------------------------------------------------------------------+
1212 # | The user can enable and configure a list of user connectors which |
1213 # | are then used by the userdb to fetch user / group information from |
1214 # | external sources like LDAP servers. |
1215 # '----------------------------------------------------------------------'
1218 def load_connection_config(lock
=False):
1219 filename
= os
.path
.join(multisite_dir
, "user_connections.mk")
1220 return store
.load_from_mk_file(filename
, "user_connections", default
=[], lock
=lock
)
1223 def save_connection_config(connections
, base_dir
=None):
1225 base_dir
= multisite_dir
1226 store
.mkdir(base_dir
)
1227 store
.save_to_mk_file(
1228 os
.path
.join(base_dir
, "user_connections.mk"), "user_connections", connections
)
1230 for connector_class
in user_connector_registry
.values():
1231 connector_class
.config_changed()
1235 # .-Hooks----------------------------------------------------------------.
1237 # | | | | | ___ ___ | | _____ |
1238 # | | |_| |/ _ \ / _ \| |/ / __| |
1239 # | | _ | (_) | (_) | <\__ \ |
1240 # | |_| |_|\___/ \___/|_|\_\___/ |
1242 # +----------------------------------------------------------------------+
1245 # This hook is called to validate the login credentials provided by a user
1246 def hook_login(username
, password
):
1247 for connection_id
, connection
in active_connections():
1248 result
= connection
.check_credentials(username
, password
)
1249 # None -> User unknown, means continue with other connectors
1250 # '<user_id>' -> success
1252 if result
not in [False, None]:
1254 if not isinstance(username
, six
.string_types
):
1255 raise MKInternalError(
1256 _("The username returned by the %s "
1257 "connector is not of type string (%r).") % (connection_id
, username
))
1258 # Check whether or not the user exists (and maybe create it)
1259 create_non_existing_user(connection_id
, username
)
1261 if not is_customer_user_allowed_to_login(username
):
1262 # A CME not assigned with the current sites customer
1263 # is not allowed to login
1264 auth_logger
.debug("User '%s' is not allowed to login: Invalid customer" % username
)
1267 # Now, after successfull login (and optional user account
1268 # creation), check whether or not the user is locked.
1269 # In e.g. htpasswd connector this is checked by validating the
1270 # password against the hash in the htpasswd file prefixed with
1271 # a "!". But when using other conectors it might be neccessary
1272 # to validate the user "locked" attribute.
1273 if connection
.is_locked(username
):
1274 auth_logger
.debug("User '%s' is not allowed to login: Account locked" % username
)
1275 return False # The account is locked
1279 elif result
is False:
1283 def show_exception(connection_id
, title
, e
, debug
=True):
1284 html
.show_error("<b>" + connection_id
+ ' - ' + title
+ "</b>"
1285 "<pre>%s</pre>" % (debug
and traceback
.format_exc() or e
))
1288 # Hook function can be registered here to be executed during saving of the
1289 # new user construct
1290 def hook_save(users
):
1291 for connection_id
, connection
in active_connections():
1293 connection
.save_users(users
)
1294 except Exception as e
:
1298 show_exception(connection_id
, _("Error during saving"), e
)
1301 # This function registers general stuff, which is independet of the single
1302 # connectors to each page load. It is exectued AFTER all other connections jobs.
1303 def general_userdb_job():
1304 # Working around the problem that the auth.php file needed for multisite based
1305 # authorization of external addons might not exist when setting up a new installation
1306 # We assume: Each user must visit this login page before using the multisite based
1307 # authorization. So we can easily create the file here if it is missing.
1308 # This is a good place to replace old api based files in the future.
1309 auth_php
= cmk
.utils
.paths
.var_dir
+ '/wato/auth/auth.php'
1310 if not os
.path
.exists(auth_php
) or os
.path
.getsize(auth_php
) == 0:
1311 cmk
.gui
.plugins
.userdb
.hook_auth
.create_auth_file("page_hook", load_users())
1313 # Create initial auth.serials file, same issue as auth.php above
1314 serials_file
= '%s/auth.serials' % os
.path
.dirname(cmk
.utils
.paths
.htpasswd_file
)
1315 if not os
.path
.exists(serials_file
) or os
.path
.getsize(serials_file
) == 0:
1316 save_users(load_users(lock
=True))
1319 def execute_userdb_job():
1320 """This function is called by the GUI cron job once a minute.
1322 Errors are logged to var/log/web.log. """
1323 if not userdb_sync_job_enabled():
1326 job
= UserSyncBackgroundJob()
1327 if job
.is_running():
1328 logger
.debug("Another synchronization job is already running: Skipping this sync")
1331 job
.set_function(job
.do_sync
, add_to_changelog
=False, enforce_sync
=False)
1335 # Legacy option config.userdb_automatic_sync defaulted to "master".
1336 # Can be: None: (no sync), "all": all sites sync, "master": only master site sync
1337 # Take that option into account for compatibility reasons.
1338 # For remote sites in distributed setups, the default is to do no sync.
1339 def user_sync_default_config(site_name
):
1340 global_user_sync
= transform_userdb_automatic_sync(config
.userdb_automatic_sync
)
1341 if global_user_sync
== "master":
1342 if config
.site_is_local(site_name
) and not config
.is_wato_slave_site():
1343 user_sync_default
= "all"
1345 user_sync_default
= None
1347 user_sync_default
= global_user_sync
1349 return user_sync_default
1352 def user_sync_config():
1353 # use global option as default for reading legacy options and on remote site
1354 # for reading the value set by the WATO master site
1355 default_cfg
= user_sync_default_config(config
.omd_site())
1356 return config
.site(config
.omd_site()).get("user_sync", default_cfg
)
1359 def userdb_sync_job_enabled():
1360 cfg
= user_sync_config()
1363 return False # not enabled at all
1365 if cfg
== "master" and config
.is_wato_slave_site():
1371 @cmk.gui
.pages
.register("ajax_userdb_sync")
1374 job
= UserSyncBackgroundJob()
1375 job
.set_function(job
.do_sync
, add_to_changelog
=False, enforce_sync
=True)
1378 except background_job
.BackgroundJobAlreadyRunning
as e
:
1379 raise MKUserError(None, _("Another user synchronization is already running: %s") % e
)
1380 html
.write('OK Started synchronization\n')
1381 except Exception as e
:
1386 html
.write('ERROR %s\n' % e
)
1389 @gui_background_job.job_registry
.register
1390 class UserSyncBackgroundJob(gui_background_job
.GUIBackgroundJob
):
1391 job_prefix
= "user_sync"
1392 gui_title
= _("User synchronization")
1396 kwargs
["title"] = self
.gui_title
1397 kwargs
["deletable"] = False
1398 kwargs
["stoppable"] = False
1400 super(UserSyncBackgroundJob
, self
).__init
__(self
.job_prefix
, **kwargs
)
1402 def _back_url(self
):
1403 return html
.makeuri_contextless([("mode", "users")], filename
="wato.py")
1405 def do_sync(self
, job_interface
, add_to_changelog
, enforce_sync
):
1406 job_interface
.send_progress_update(_("Synchronization started..."))
1407 if self
._execute
_sync
_action
(job_interface
, add_to_changelog
, enforce_sync
):
1408 job_interface
.send_result_message(_("The user synchronization completed successfully."))
1410 job_interface
.send_exception(_("The user synchronization failed."))
1412 def _execute_sync_action(self
, job_interface
, add_to_changelog
, enforce_sync
):
1413 for connection_id
, connection
in active_connections():
1415 if not enforce_sync
and not connection
.sync_is_needed():
1418 job_interface
.send_progress_update(
1419 _("[%s] Starting sync for connection") % connection_id
)
1420 connection
.do_sync(add_to_changelog
=add_to_changelog
, only_username
=False)
1421 job_interface
.send_progress_update(
1422 _("[%s] Finished sync for connection") % connection_id
)
1423 except Exception as e
:
1424 job_interface
.send_exception(_("[%s] Exception: %s") % (connection_id
, e
))
1426 'Exception (%s, userdb_job): %s' % (connection_id
, traceback
.format_exc()))
1428 job_interface
.send_progress_update(_("Finalizing synchronization"))
1429 general_userdb_job()