# -*- coding: utf-8 -*-
#
# 2019-07-01 Cornelius Kölbel <cornelius.koelbel@netknights.it>
# Add admin read policies
# 2019-06-19 Friedrich Weber <friedrich.weber@netknights.it>
# Add handling of policy conditions
# 2019-05-25 Cornelius Kölbel <cornelius.koelbel@netknights.it>
# Add max_active_token_per_user
# 2019-05-23 Cornelius Kölbel <cornelius.koelbel@netknights.it>
# Add passthru_assign policy
# 2018-09-07 Cornelius Kölbel <cornelius.koelbel@netknights.it>
# Add App Image URL
# 2018-01-15 Cornelius Kölbel <cornelius.koelbel@netknights.it>
# Add tokeninfo field policy
# Add add_resolver_in_result
# 2017-11-14 Cornelius Kölbel <cornelius.koelbel@netknights.it>
# Add policy action for customization of menu and baseline
# 2017-01-22 Cornelius Kölbel <cornelius.koelbel@netknights.it>
# Add policy action groups
# 2016-12-19 Cornelius Kölbel <cornelius.koelbel@netknights.it>
# Add check_all_resolvers logic
# 2016-11-20 Cornelius Kölbel <cornelius.koelbel@netknights.it>
# Add audit log age functionality
# 2016-08-30 Cornelius Kölbel <cornelius.koelbel@netknights.it>
# Add registration body
# 2016-06-21 Cornelius Kölbel <cornelius.koelbel@netknights.it>
# Change PIN policies
# 2016-05-07 Cornelius Kölbel <cornelius.koelbel@netknights.it>
# Add realm dropdown
# 2016-04-06 Cornelius Kölbel <cornelius.koelbel@netknights.it>
# Add time dependency in policy
# 2016-02-22 Cornelius Kölbel <cornelius.koelbel@netknights.it>
# Add RADIUS passthru policy
# 2016-02-05 Cornelius Kölbel <cornelius.koelbel@netknights.it>
# Add tokenwizard in scope UI
# 2015-12-30 Cornelius Kölbel <cornelius.koelbel@netknights.it>
# Add password reset policy
# 2015-12-28 Cornelius Kölbel <cornelius.koelbel@netknights.it>
# Add registration policy
# 2015-12-16 Cornelius Kölbel <cornelius.koelbel@netknights.it>
# Add tokenissuer policy
# 2015-11-29 Cornelius Kölbel <cornelius.koelbel@netknights.it>
# Add getchallenges policy
# 2015-10-31 Cornelius Kölbel <cornelius.koelbel@netknights.it>
# Add last_auth policy.
# 2015-10-30 Cornelius Kölbel <cornelius.koelbel@netknights.it>
# Display user details in token list
# 2015-10-26 Cornelius Kölbel <cornelius.koelbel@netknights.it>
# Add default token type for enrollment
# 2015-10-14 Cornelius Kölbel <cornelius.koelbel@netknights.it>
# Add auth_max_success and auth_max_fail actions to
# scope authorization
# 2015-10-09 Cornelius Kölbel <cornelius.koelbel@netknights.it>
# Add token_page_size and user_page_size policy
# 2015-09-06 Cornelius Kölbel <cornelius.koelbel@netkngihts.it>
# Add challenge_response authentication policy
# 2015-06-30 Cornelius Kölbel <cornelius.koelbel@netknights.it>
# Add the OTP PIN handling
# 2015-06-29 Cornelius Kölbel <cornelius.koelbel@netknights.it>
# Add the mangle policy
# 2015-04-03 Cornelius Kölbel <cornelius.koelbel@netknights.it>
# Add WebUI logout time.
# 2015-03-27 Cornelius Kölbel <cornelius.koelbel@netknights.it>
# Add PIN policies in USER scope
# 2015-02-06 Cornelius Kölbel <cornelius@privacyidea.org>
# Rewrite for flask migration.
# Policies are not handled by decorators as
# 1. precondition for API calls
# 2. internal modifications of LIB-functions
# 3. postcondition for API calls
#
# Jul 07, 2014 add check_machine_policy, Cornelius Kölbel
# May 08, 2014 Cornelius Kölbel
#
# License: AGPLv3
# contact: http://www.privacyidea.org
#
# privacyIDEA is a fork of LinOTP
# Copyright (C) 2010 - 2014 LSE Leading Security Experts GmbH
# License: AGPLv3
# contact: http://www.linotp.org
# http://www.lsexperts.de
# linotp@lsexperts.de
#
# This code is free software; you can redistribute it and/or
# modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE
# License as published by the Free Software Foundation; either
# version 3 of the License, or any later version.
#
# This code is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU AFFERO GENERAL PUBLIC LICENSE for more details.
#
# You should have received a copy of the GNU Affero General Public
# License along with this program. If not, see <http://www.gnu.org/licenses/>.
#
"""
Base function to handle the policy entries in the database.
This module only depends on the db/models.py
The functions of this module are tested in tests/test_lib_policy.py
A policy has the attributes
* name
* scope
* action
* realm
* resolver
* user
* client
* active
``name`` is the unique identifier of a policy. ``scope`` is the area,
where this policy is meant for. This can be values like admin, selfservice,
authentication...
``scope`` takes only one value.
``active`` is bool and indicates, whether a policy is active or not.
``action``, ``realm``, ``resolver``, ``user`` and ``client`` can take a comma
separated list of values.
realm and resolver
------------------
If these are empty '*', this policy matches each requested realm.
user
----
If the user is empty or '*', this policy matches each user.
You can exclude users from matching this policy, by prepending a '-' or a '!'.
``*, -admin`` will match for all users except the admin.
You can also use regular expressions to match the user like ``customer_.*``
to match any user, starting with *customer_*.
.. note:: Regular expression will only work for exact machtes.
*user1234* will not match *user1* but only *user1...*
client
------
The client is identified by its IP address. A policy can contain a list of
IP addresses or subnets.
You can exclude clients from subnets by prepending the client with a '-' or
a '!'.
``172.16.0.0/24, -172.16.0.17`` will match each client in the subnet except
the 172.16.0.17.
time
----
You can specify a time in which the policy should be active.
Time formats are
<dow>-<dow>:<hh>:<mm>-<hh>:<mm>, ...
<dow>:<hh>:<mm>-<hh>:<mm>
<dow>:<hh>-<hh>
and any combination of it. "dow" being day of week Mon, Tue, Wed, Thu, Fri,
Sat, Sun.
"""
from .log import log_with
from configobj import ConfigObj
from operator import itemgetter
import six
import logging
from ..models import (Policy, db, save_config_timestamp)
from privacyidea.lib.config import (get_token_classes, get_token_types,
get_config_object)
from privacyidea.lib.framework import get_app_config_value
from privacyidea.lib.error import ParameterError, PolicyError, ResourceNotFoundError
from privacyidea.lib.realm import get_realms
from privacyidea.lib.resolver import get_resolver_list
from privacyidea.lib.smtpserver import get_smtpservers
from privacyidea.lib.radiusserver import get_radiusservers
from privacyidea.lib.utils import (check_time_in_range, reload_db,
fetch_one_resource, is_true, check_ip_in_policy)
from privacyidea.lib.utils.compare import compare_values, CompareError, COMPARATOR_FUNCTIONS, COMPARATORS, \
COMPARATOR_DESCRIPTIONS
from privacyidea.lib.user import User
from privacyidea.lib import _
import datetime
import re
import ast
from six import with_metaclass, string_types
log = logging.getLogger(__name__)
optional = True
required = False
[docs]class SCOPE(object):
__doc__ = """This is the list of the allowed scopes that can be used in
policy definitions.
"""
AUTHZ = "authorization"
ADMIN = "admin"
AUTH = "authentication"
AUDIT = "audit"
USER = "user" # was selfservice
ENROLL = "enrollment"
GETTOKEN = "gettoken"
WEBUI = "webui"
REGISTER = "register"
[docs]class ACTION(object):
__doc__ = """This is the list of usual actions."""
ASSIGN = "assign"
APPIMAGEURL = "appimageurl"
AUDIT = "auditlog"
AUDIT_AGE = "auditlog_age"
AUDIT_DOWNLOAD = "auditlog_download"
AUTHITEMS = "fetch_authentication_items"
AUTHMAXSUCCESS = "auth_max_success"
AUTHMAXFAIL = "auth_max_fail"
AUTOASSIGN = "autoassignment"
CACONNECTORREAD = "caconnectorread"
CACONNECTORWRITE = "caconnectorwrite"
CACONNECTORDELETE = "caconnectordelete"
CHALLENGERESPONSE = "challenge_response"
CHALLENGETEXT = "challenge_text"
CHALLENGETEXT_HEADER = "challenge_text_header"
CHALLENGETEXT_FOOTER = "challenge_text_footer"
GETCHALLENGES = "getchallenges"
COPYTOKENPIN = "copytokenpin"
COPYTOKENUSER = "copytokenuser"
DEFAULT_TOKENTYPE = "default_tokentype"
DELETE = "delete"
DISABLE = "disable"
EMAILCONFIG = "smtpconfig"
ENABLE = "enable"
ENCRYPTPIN = "encrypt_pin"
FORCE_APP_PIN = "force_app_pin"
GETSERIAL = "getserial"
GETRANDOM = "getrandom"
IMPORT = "importtokens"
LASTAUTH = "last_auth"
LOGINMODE = "login_mode"
LOGOUTTIME = "logout_time"
LOSTTOKEN = 'losttoken'
LOSTTOKENPWLEN = "losttoken_PW_length"
LOSTTOKENPWCONTENTS = "losttoken_PW_contents"
LOSTTOKENVALID = "losttoken_valid"
MACHINERESOLVERWRITE = "mresolverwrite"
MACHINERESOLVERDELETE = "mresolverdelete"
MACHINERESOLVERREAD = "mresolverread"
MACHINELIST = "machinelist"
MACHINETOKENS = "manage_machine_tokens"
MANGLE = "mangle"
MAXTOKENREALM = "max_token_per_realm"
MAXTOKENUSER = "max_token_per_user"
MAXACTIVETOKENUSER = "max_active_token_per_user"
NODETAILSUCCESS = "no_detail_on_success"
ADDUSERINRESPONSE = "add_user_in_response"
ADDRESOLVERINRESPONSE = "add_resolver_in_response"
NODETAILFAIL = "no_detail_on_fail"
OTPPIN = "otppin"
OTPPINRANDOM = "otp_pin_random"
OTPPINMAXLEN = 'otp_pin_maxlength'
OTPPINMINLEN = 'otp_pin_minlength'
OTPPINCONTENTS = 'otp_pin_contents'
PASSNOTOKEN = "passOnNoToken"
PASSNOUSER = "passOnNoUser"
PASSTHRU = "passthru"
PASSTHRU_ASSIGN = "passthru_assign"
PASSWORDRESET = "password_reset"
PINHANDLING = "pinhandling"
POLICYDELETE = "policydelete"
POLICYWRITE = "policywrite"
POLICYREAD = "policyread"
POLICYTEMPLATEURL = "policy_template_url"
REALM = "realm"
REMOTE_USER = "remote_user"
REQUIREDEMAIL = "requiredemail"
RESET = "reset"
RESOLVERDELETE = "resolverdelete"
RESOLVERWRITE = "resolverwrite"
RESOLVERREAD = "resolverread"
RESOLVER = "resolver"
RESYNC = "resync"
REVOKE = "revoke"
SET = "set"
SETDESCRIPTION = "setdescription"
SETPIN = "setpin"
SETREALM = "setrealm"
SERIAL = "serial"
SYSTEMDELETE = "configdelete"
SYSTEMWRITE = "configwrite"
SYSTEMREAD = "configread"
CONFIGDOCUMENTATION = "system_documentation"
SETTOKENINFO = "settokeninfo"
TOKENISSUER = "tokenissuer"
TOKENLABEL = "tokenlabel"
TOKENLIST = "tokenlist"
TOKENPAGESIZE = "token_page_size"
TOKENREALMS = "tokenrealms"
TOKENTYPE = "tokentype"
TOKENINFO = "tokeninfo"
TOKENWIZARD = "tokenwizard"
TOKENWIZARD2ND = "tokenwizard_2nd_token"
TRIGGERCHALLENGE = "triggerchallenge"
UNASSIGN = "unassign"
USERLIST = "userlist"
USERPAGESIZE = "user_page_size"
ADDUSER = "adduser"
DELETEUSER = "deleteuser"
UPDATEUSER = "updateuser"
USERDETAILS = "user_details"
APIKEY = "api_key_required"
SETHSM = "set_hsm_password"
SMTPSERVERWRITE = "smtpserver_write"
SMTPSERVERREAD = "smtpserver_read"
RADIUSSERVERWRITE = "radiusserver_write"
RADIUSSERVERREAD = "radiusserver_read"
PRIVACYIDEASERVERWRITE = "privacyideaserver_write"
PRIVACYIDEASERVERREAD = "privacyideaserver_read"
REALMDROPDOWN = "realm_dropdown"
EVENTHANDLINGWRITE = "eventhandling_write"
EVENTHANDLINGREAD = "eventhandling_read"
PERIODICTASKWRITE = "periodictask_write"
PERIODICTASKREAD = "periodictask_read"
SMSGATEWAYWRITE = "smsgateway_write"
SMSGATEWAYREAD = "smsgateway_read"
CHANGE_PIN_FIRST_USE = "change_pin_on_first_use"
CHANGE_PIN_EVERY = "change_pin_every"
CLIENTTYPE = "clienttype"
REGISTERBODY = "registration_body"
RESETALLTOKENS = "reset_all_user_tokens"
ENROLLPIN = "enrollpin"
MANAGESUBSCRIPTION = "managesubscription"
SEARCH_ON_ENTER = "search_on_enter"
TIMEOUT_ACTION = "timeout_action"
AUTH_CACHE = "auth_cache"
HIDE_BUTTONS = "hide_buttons"
HIDE_WELCOME = "hide_welcome_info"
SHOW_SEED = "show_seed"
CUSTOM_MENU = "custom_menu"
CUSTOM_BASELINE = "custom_baseline"
STATISTICSREAD = "statistics_read"
STATISTICSDELETE = "statistics_delete"
LOGIN_TEXT = "login_text"
DIALOG_NO_TOKEN = "dialog_no_token"
[docs]class GROUP(object):
__doc__ = """These are the allowed policy action groups. The policies
will be grouped in the UI."""
TOOLS = "tools"
SYSTEM = "system"
TOKEN = "token"
ENROLLMENT = "enrollment"
GENERAL = "general"
MACHINE = "machine"
USER = "user"
PIN = "pin"
[docs]class MAIN_MENU(object):
__doc__ = """These are the allowed top level menu items. These are used
to toggle the visibility of the menu items depending on the rights of the
user"""
TOKENS = "tokens"
USERS = "users"
MACHINES = "machines"
CONFIG = "config"
AUDIT = "audit"
COMPONENTS = "components"
[docs]class LOGINMODE(object):
__doc__ = """This is the list of possible values for the login mode."""
USERSTORE = "userstore"
PRIVACYIDEA = "privacyIDEA"
DISABLE = "disable"
[docs]class REMOTE_USER(object):
__doc__ = """The list of possible values for the remote_user policy."""
DISABLE = "disable"
ACTIVE = "allowed"
[docs]class ACTIONVALUE(object):
__doc__ = """This is a list of usual action values for e.g. policy
action-values like otppin."""
TOKENPIN = "tokenpin"
USERSTORE = "userstore"
DISABLE = "disable"
NONE = "none"
[docs]class AUTOASSIGNVALUE(object):
__doc__ = """This is the possible values for autoassign"""
USERSTORE = "userstore"
NONE = "any_pin"
[docs]class TIMEOUT_ACTION(object):
__doc__ = """This is a list of actions values for idle users"""
LOGOUT = "logout"
LOCKSCREEN = 'lockscreen'
[docs]class CONDITION_SECTION(object):
__doc__ = """This is a list of available sections for conditions of policies """
USERINFO = "userinfo"
[docs]class PolicyClass(object):
"""
A policy object can be used to query the current set of policies.
The policy object itself does not store any policies.
Instead, every query uses ``get_config_object`` to retrieve the request-local
config object which contains the current set of policies.
Hence, reloading the request-local config object also reloads the set of policies.
"""
def __init__(self):
pass
@property
def policies(self):
"""
Shorthand to retrieve the set of policies of the request-local config object
"""
return get_config_object().policies
@classmethod
def _search_value(cls, policy_attributes, searchvalue):
"""
Searches a given value in a policy attribute. The policy_attribute is
a list like searching the resolver name "resolver1" in the given
resolvers of a policy:
policy.get("resolver") = ["resolver1", "resolver2"]
It returns a tuple of booleans if the searched value is
contained/found or excluded.
:param policy_attributes:
:param searchvalue:
:return: tuple of value_found and value_excluded
"""
value_found = False
value_excluded = False
for value in policy_attributes:
if value and value[0] in ["!", "-"] and \
searchvalue == value[1:]:
value_excluded = True
elif type(searchvalue) == list and value in \
searchvalue + ["*"]:
value_found = True
elif value in [searchvalue, "*"]:
value_found = True
elif type(searchvalue) != list:
# Do not do this search style for resolvers, which come as a
# list
# check regular expression only for exact matches
# avoid matching user1234 -> user1
if re.search(u"^{0!s}$".format(value), searchvalue):
value_found = True
return value_found, value_excluded
[docs] @log_with(log)
def list_policies(self, name=None, scope=None, realm=None, active=None,
resolver=None, user=None, client=None, action=None,
adminrealm=None, sort_by_priority=True):
"""
Return the policies, filtered by the given values.
The following rule holds for all filter arguments:
If ``None`` is passed as a value, policies are not filtered according to the
argument at all. As an example, if ``realm=None`` is passed,
policies are matched regardless of their ``realm`` attribute.
If any value is passed (even the empty string), policies are filtered
according to the given value. As an example, if ``realm=''`` is passed,
only policies that have a matching (or empty) realm attribute are returned.
The only exception is the ``client`` parameter, which does not accept the empty string,
and throws a ParameterError if the empty string is passed.
:param name: The name of the policy
:param scope: The scope of the policy
:param realm: The realm in the policy
:param active: One of None, True, False: All policies, only active or only inactive policies
:param resolver: Only policies with this resolver
:param user: Only policies with this user
:type user: basestring
:param client:
:param action: Only policies, that contain this very action.
:param adminrealm: This is the realm of the admin. This is only
evaluated in the scope admin.
:param sort_by_priority: If true, sort the resulting list by priority, ascending
by their policy numbers.
:type sort_by_priority: bool
:return: list of policies
:rtype: list of dicts
"""
reduced_policies = self.policies
# Do exact matches for "name", "active" and "scope", as these fields
# can only contain one entry
p = [("name", name), ("active", active), ("scope", scope)]
for searchkey, searchvalue in p:
if searchvalue is not None:
reduced_policies = [policy for policy in reduced_policies if
policy.get(searchkey) == searchvalue]
log.debug("Policies after matching {1!s}: {0!s}".format(
reduced_policies, searchkey))
p = [("action", action), ("user", user), ("realm", realm)]
# If this is an admin-policy, we also do check the adminrealm
if scope == "admin":
p.append(("adminrealm", adminrealm))
for searchkey, searchvalue in p:
if searchvalue is not None:
new_policies = []
# first we find policies, that really match!
# Either with the real value or with a "*"
# values can be excluded by a leading "!" or "-"
for policy in reduced_policies:
if not policy.get(searchkey):
# We also find the policies with no distinct information
# about the request value
new_policies.append(policy)
else:
value_found, value_excluded = self._search_value(
policy.get(searchkey), searchvalue)
if value_found and not value_excluded:
new_policies.append(policy)
reduced_policies = new_policies
log.debug("Policies after matching {1!s}: {0!s}".format(
reduced_policies, searchkey))
# We need to act individually on the resolver key word
# We either match the resolver exactly or we match another resolver (
# which is not the first resolver) of the user, but only if the
# check_all_resolvers flag in the policy is set.
if resolver is not None:
new_policies = []
user_resolvers = []
for policy in reduced_policies:
if policy.get("check_all_resolvers"):
if realm and user:
# We have a realm and a user and can get all resolvers
# of this user in the realm
if not user_resolvers:
user_resolvers = User(user,
realm=realm).get_ordererd_resolvers()
for reso in user_resolvers:
value_found, _v_ex = self._search_value(
policy.get("resolver"), reso)
if value_found:
new_policies.append(policy)
break
elif not policy.get("resolver"):
# We also find the policies with no distinct information
# about the request value
new_policies.append(policy)
else:
value_found, _v_ex = self._search_value(
policy.get("resolver"), resolver)
if value_found:
new_policies.append(policy)
reduced_policies = new_policies
log.debug("Policies after matching resolver: {0!s}".format(
reduced_policies))
# Match the client IP.
# Client IPs may be direct match, may be located in subnets or may
# be excluded by a leading "-" or "!" sign.
# The client definition in the policy may ba a comma separated list.
# It may start with a "-" or a "!" to exclude the client
# from a subnet.
# Thus a client 10.0.0.2 matches a policy "10.0.0.0/8, -10.0.0.1" but
# the client 10.0.0.1 does not match the policy "10.0.0.0/8, -10.0.0.1".
# An empty client definition in the policy matches all clients.
if client is not None:
if not client:
raise ParameterError("client argument must be a non-empty string")
new_policies = []
for policy in reduced_policies:
log.debug(u"checking client ip in policy {0!s}.".format(policy))
client_found, client_excluded = check_ip_in_policy(client, policy.get("client"))
if client_found and not client_excluded:
# The client was contained in the defined subnets and was
# not excluded
new_policies.append(policy)
# If there is a policy without any client, we also add it to the
# accepted list.
for policy in reduced_policies:
if not policy.get("client"):
new_policies.append(policy)
reduced_policies = new_policies
log.debug("Policies after matching client".format(
reduced_policies))
if sort_by_priority:
reduced_policies = sorted(reduced_policies, key=itemgetter("priority"))
return reduced_policies
[docs] def match_policies(self, name=None, scope=None, realm=None, active=None,
resolver=None, user=None, user_object=None,
client=None, action=None, adminrealm=None, time=None,
sort_by_priority=True, audit_data=None):
"""
Return all policies matching the given context.
Optionally, write the matching policies to the audit log.
In order to retrieve policies matching the current user,
callers can *either* pass a user(name), resolver and realm,
*or* pass a user object from which login name, resolver and realm will be read.
In case of conflicting parameters, a ParameterError will be raised.
This function takes all parameters taken by ``list_policies``, plus
some additional parameters.
:param name: see ``list_policies``
:param scope: see ``list_policies``
:param realm: see ``list_policies``
:param active: see ``list_policies``
:param resolver: see ``list_policies``
:param user: see ``list_policies``
:param client: see ``list_policies``
:param action: see ``list_policies``
:param adminrealm: see ``list_policies``
:param sort_by_priority:
:param user_object: the currently active user, or None
:type user_object: User or None
:param time: return only policies that are valid at the specified time. Defaults to the current time.
:type time: datetime or None
:param audit_data: A dictionary with audit data collected during a request. This
method will add found policies to the dictionary.
:type audit_data: dict or None
:return: a list of policy dictionaries
"""
if user_object is not None:
if not (user is None and realm is None and resolver is None):
raise ParameterError("Cannot pass user_object as well as user, resolver, realm")
user = user_object.login
realm = user_object.realm
resolver = user_object.resolver
reduced_policies = self.list_policies(name=name, scope=scope, realm=realm, active=active,
resolver=resolver, user=user, client=client, action=action,
adminrealm=adminrealm, sort_by_priority=sort_by_priority)
# filter policy for time. If no time is set or is a time is set and
# it matches the time_range, then we add this policy
reduced_policies = [policy for policy in reduced_policies if
(policy.get("time") and
check_time_in_range(policy.get("time"), time))
or not policy.get("time")]
log.debug("Policies after matching time: {0!s}".format(
reduced_policies))
# filter policies by the policy conditions
reduced_policies = self.filter_policies_by_conditions(reduced_policies, user_object)
log.debug("Policies after matching conditions".format(
reduced_policies))
if audit_data is not None:
for p in reduced_policies:
audit_data.setdefault("policies", []).append(p.get("name"))
return reduced_policies
[docs] def filter_policies_by_conditions(self, policies, user_object=None):
"""
Given a list of policy dictionaries and a current user object (if any),
return a list of all policies whose conditions match the given user object.
Raises a PolicyError if a condition references an unknown section.
:param policies: a list of policy dictionaries
:param user_object: a User object, or None if there is no current user
:return: generates a list of policy dictionaries
"""
reduced_policies = []
for policy in policies:
include_policy = True
for section, key, comparator, value, active in policy['conditions']:
if active:
if section == CONDITION_SECTION.USERINFO:
if not self._policy_matches_userinfo_condition(policy, key, comparator, value, user_object):
include_policy = False
break
else:
log.warning(u"Policy {!r} has condition with unknown section: {!r}".format(
policy['name'], section
))
raise PolicyError(u"Policy {!r} has condition with unknown section".format(policy['name']))
if include_policy:
reduced_policies.append(policy)
return reduced_policies
@staticmethod
def _policy_matches_userinfo_condition(policy, key, comparator, value, user_object):
"""
Check if the given policy matches a certain userinfo condition.
If ``user_object`` is None, a PolicyError is raised.
:param policy: a policy dictionary, the policy in question
:param key: a userinfo key
:param comparator: a value comparator: one of "equal", "contains"
:param value: a value against which the userinfo value will be compared
:param user_object: a User object, if any, or None
:return: a Boolean
"""
# Match the user object's user info, if it is not-None and non-empty
if user_object is not None:
info = user_object.info
if key in info:
try:
return compare_values(info[key], comparator, value)
except Exception as exx:
log.warning(u"Error during handling the condition on userinfo {!r} of policy {!r}: {!r}".format(
key, policy['name'], exx
))
raise PolicyError(
u"Invalid comparison in the userinfo conditions of policy {!r}".format(policy['name']))
else:
# If we do have a user object, but the conditions of policies reference
# an unknown userinfo key, we have a misconfiguration and raise an error.
log.warning(u"Unknown userinfo key referenced in a condition of policy {!r}: {!r}".format(
policy['name'], key
))
raise PolicyError(u"Unknown key in the userinfo conditions of policy {!r}".format(
policy['name']
))
else:
log.warning(u"Policy {!r} has condition on userinfo {!r}, but userinfo is not available".format(
policy['name'], key
))
# If the policy specifies a userinfo condition, but no user object is available,
# the policy is misconfigured. We have to raise a PolicyError to ensure that
# the privacyIDEA server does not silently misbehave.
raise PolicyError(
u"Policy {!r} has condition on userinfo, but userinfo is not available".format(
policy['name']
))
[docs] @staticmethod
def check_for_conflicts(policies, action):
"""
Given a (not necessarily sorted) list of policy dictionaries and an action name,
check that there are no action value conflicts.
This raises a PolicyError if there are multiple policies with the highest
priority which define different values for **action**.
Otherwise, the function just returns nothing.
:param policies: list of dictionaries
:param action: string
"""
if len(policies) > 1:
prioritized_policy = min(policies, key=itemgetter("priority"))
prioritized_action = prioritized_policy["action"][action]
highest_priority = prioritized_policy["priority"]
for other_policy in policies:
if (other_policy["priority"] == highest_priority
and other_policy["action"][action] != prioritized_action):
raise PolicyError("Contradicting {!s} policies.".format(action))
[docs] @log_with(log)
def get_action_values(self, action, scope=SCOPE.AUTHZ, realm=None,
resolver=None, user=None, client=None, unique=False,
allow_white_space_in_action=False, adminrealm=None,
user_object=None, audit_data=None):
"""
Get the defined action values for a certain action like
scope: authorization
action: tokentype
would return a dictionary of {tokentype: policyname}
scope: authorization
action: serial
would return a dictionary of {serial: policyname}
All parameters not described below are covered in the documentation of ``match_policies``.
:param unique: if set, the function will only consider the policy with the
highest priority and check for policy conflicts.
:param allow_white_space_in_action: Some policies like emailtext
would allow entering text with whitespaces. These whitespaces
must not be used to separate action values!
:type allow_white_space_in_action: bool
:param audit_data: This is a dictionary, that can take audit_data in the g object.
If set, this dictionary will be filled with the list of triggered policynames in the
key "policies". This can be useful for policies like ACTION.OTPPIN - where it is clear, that the
found policy will be used. I could make less sense with an aktion like ACTION.LASTAUTH - where
the value of the action needs to be evaluated in a more special case.
:rtype: dict
"""
policy_values = {}
policies = self.match_policies(scope=scope, adminrealm=adminrealm,
action=action, active=True,
realm=realm, resolver=resolver, user=user, user_object=user_object,
client=client, sort_by_priority=True)
# If unique = True, only consider the policies with the highest priority
if policies and unique:
highest_priority = policies[0]['priority']
policies = [p for p in policies if p['priority'] == highest_priority]
for pol in policies:
action_dict = pol.get("action", {})
action_value = action_dict.get(action, "")
policy_name = pol.get("name")
"""
We must distinguish actions like:
tokentype=totp hotp motp,
where the string represents a list divided by spaces, and
smstext='your otp is <otp>'
where the spaces are part of the string.
"""
# By saving the policynames in a dict with the values being the key,
# we achieve unique policy_values.
# Save the policynames in a list
if action_value.startswith("'") and action_value.endswith("'"):
action_key = action_dict.get(action)[1:-1]
policy_values.setdefault(action_key, []).append(policy_name)
elif allow_white_space_in_action:
action_key = action_dict.get(action)
policy_values.setdefault(action_key, []).append(policy_name)
else:
for action_key in action_dict.get(action, "").split():
policy_values.setdefault(action_key, []).append(policy_name)
# Check if the policies with the highest priority agree on the action values
if unique and len(policy_values) > 1:
names = [p['name'] for p in policies]
raise PolicyError(u"There are policies with conflicting actions: {!r}".format(names))
if audit_data is not None:
for action_value, policy_names in policy_values.items():
for p_name in policy_names:
audit_data.setdefault("policies", []).append(p_name)
return policy_values
[docs] @log_with(log)
def ui_get_main_menus(self, logged_in_user, client=None):
"""
Get the list of allowed main menus derived from the policies for the
given user - admin or normal user.
It fetches all policies for this user and compiles a list of allowed
menus to display or hide in the UI.
:param logged_in_user: The logged in user, a dictionary with keys
"username", "realm" and "role".
:param client: The IP address of the client
:return: A list of MENUs to be displayed
"""
from privacyidea.lib.token import get_dynamic_policy_definitions
role = logged_in_user.get("role")
user_rights = self.ui_get_rights(role,
logged_in_user.get("realm"),
logged_in_user.get("username"),
client)
main_menus = []
static_rights = get_static_policy_definitions(role)
enroll_rights = get_dynamic_policy_definitions(role)
static_rights.update(enroll_rights)
for r in user_rights:
menus = static_rights.get(r, {}).get("mainmenu", [])
main_menus.extend(menus)
main_menus = list(set(main_menus))
return main_menus
[docs] @log_with(log)
def ui_get_rights(self, scope, realm, username, client=None):
"""
Get the rights derived from the policies for the given realm and user.
Works for admins and normal users.
It fetches all policies for this user and compiles a maximum list of
allowed rights, that can be used to hide certain UI elements.
:param scope: Can be SCOPE.ADMIN or SCOPE.USER
:param realm: Is either user users realm or the adminrealm
:param username: The loginname of the user
:param client: The HTTP client IP
:return: A list of actions
"""
from privacyidea.lib.token import get_dynamic_policy_definitions
rights = set()
if scope == SCOPE.ADMIN:
# If the logged-in user is an admin, we match for username/adminrealm only
match_username = username
user_object = None
adminrealm = realm
elif scope == SCOPE.USER:
# If the logged-in user is a user, we pass a user object to allow matching for userinfo attributes
match_username = None
adminrealm = None
user_object = User(username, realm)
else:
raise PolicyError(u"Unknown scope: {}".format(scope))
pols = self.match_policies(scope=scope,
user=match_username,
user_object=user_object,
adminrealm=adminrealm,
active=True,
client=client)
for pol in pols:
for action, action_value in pol.get("action").items():
if action_value:
rights.add(action)
# if the action has an actual non-boolean value, return it
if isinstance(action_value, string_types):
rights.add(u"{}={}".format(action, action_value))
# check if we have policies at all:
pols = self.list_policies(scope=scope, active=True)
if not pols:
# We do not have any policies in this scope, so we return all
# possible actions in this scope.
log.debug("No policies defined, so we set all rights.")
rights = get_static_policy_definitions(scope)
rights.update(get_dynamic_policy_definitions(scope))
rights = list(rights)
log.debug("returning the admin rights: {0!s}".format(rights))
return rights
[docs] @log_with(log)
def ui_get_enroll_tokentypes(self, client, logged_in_user):
"""
Return a dictionary of the allowed tokentypes for the logged in user.
This used for the token enrollment UI.
It looks like this:
{"hotp": "HOTP: event based One Time Passwords",
"totp": "TOTP: time based One Time Passwords",
"spass": "SPass: Simple Pass token. Static passwords",
"motp": "mOTP: classical mobile One Time Passwords",
"sshkey": "SSH Public Key: The public SSH key",
"yubikey": "Yubikey AES mode: One Time Passwords with Yubikey",
"remote": "Remote Token: Forward authentication request to another server",
"yubico": "Yubikey Cloud mode: Forward authentication request to YubiCloud",
"radius": "RADIUS: Forward authentication request to a RADIUS server",
"email": "EMail: Send a One Time Passwort to the users email address",
"sms": "SMS: Send a One Time Password to the users mobile phone",
"certificate": "Certificate: Enroll an x509 Certificate Token."}
:param client: Client IP address
:type client: basestring
:param logged_in_user: The Dict of the logged in user
:type logged_in_user: dict
:return: list of token types, the user may enroll
"""
from privacyidea.lib.auth import ROLE
enroll_types = {}
role = logged_in_user.get("role")
if role == ROLE.ADMIN:
# If the logged-in user is an admin, we match for username/adminrealm
user_name = logged_in_user.get("username")
user_object = None
admin_realm = logged_in_user.get("realm")
else:
# If the logged-in user is a user, we pass an user object to allow matching for userinfo attributes
user_name = None
user_object = User(logged_in_user.get("username"),
logged_in_user.get("realm"))
admin_realm = None
# check, if we have a policy definition at all.
pols = self.list_policies(scope=role, active=True)
tokenclasses = get_token_classes()
for tokenclass in tokenclasses:
# Check if the tokenclass is ui enrollable for "user" or "admin"
if role in tokenclass.get_class_info("ui_enroll"):
enroll_types[tokenclass.get_class_type()] = \
tokenclass.get_class_info("description")
if pols:
# admin policies or user policies are set, so we need to
# test, which tokens are allowed to be enrolled for this user
filtered_enroll_types = {}
for tokentype in enroll_types.keys():
# determine, if there is a enrollment policy for this very type
typepols = self.match_policies(scope=role, client=client,
user=user_name,
user_object=user_object,
active=True,
action="enroll"+tokentype.upper(),
adminrealm=admin_realm)
if typepols:
# If there is no policy allowing the enrollment of this
# tokentype, it is deleted.
filtered_enroll_types[tokentype] = enroll_types[tokentype]
enroll_types = filtered_enroll_types
return enroll_types
# --------------------------------------------------------------------------
#
# NEW STUFF
#
#
[docs]@log_with(log)
def set_policy(name=None, scope=None, action=None, realm=None, resolver=None,
user=None, time=None, client=None, active=True,
adminrealm=None, priority=None, check_all_resolvers=False,
conditions=None):
"""
Function to set a policy.
If the policy with this name already exists, it updates the policy.
It expects a dict of with the following keys:
:param name: The name of the policy
:param scope: The scope of the policy. Something like "admin" or "authentication"
:param action: A scope specific action or a comma separated list of actions
:type active: basestring
:param realm: A realm, for which this policy is valid
:param resolver: A resolver, for which this policy is valid
:param user: A username or a list of usernames
:param time: N/A if type()
:param client: A client IP with optionally a subnet like 172.16.0.0/16
:param active: If the policy is active or not
:type active: bool
:param priority: the priority of the policy (smaller values having higher priority)
:type priority: int
:param check_all_resolvers: If all the resolvers of a user should be
checked with this policy
:type check_all_resolvers: bool
:param conditions: A list of 5-tuples (section, key, comparator, value, active) of policy conditions
:return: The database ID od the the policy
:rtype: int
"""
active = is_true(active)
if isinstance(priority, six.string_types):
priority = int(priority)
if priority is not None and priority <= 0:
raise ParameterError("Priority must be at least 1")
check_all_resolvers = is_true(check_all_resolvers)
if type(action) == dict:
action_list = []
for k, v in action.items():
if v is not True:
# value key
action_list.append("{0!s}={1!s}".format(k, v))
else:
# simple boolean value
action_list.append(k)
action = ", ".join(action_list)
if type(action) == list:
action = ", ".join(action)
if type(realm) == list:
realm = ", ".join(realm)
if type(adminrealm) == list:
adminrealm = ", ".join(adminrealm)
if type(user) == list:
user = ", ".join(user)
if type(resolver) == list:
resolver = ", ".join(resolver)
if type(client) == list:
client = ", ".join(client)
# validate conditions parameter
if conditions is not None:
for condition in conditions:
if len(condition) != 5:
raise ParameterError(u"Conditions must be 5-tuples: {!r}".format(condition))
if not (isinstance(condition[0], six.string_types)
and isinstance(condition[1], six.string_types)
and isinstance(condition[2], six.string_types)
and isinstance(condition[3], six.string_types)
and isinstance(condition[4], bool)):
raise ParameterError(u"Conditions must be 5-tuples of four strings and one boolean: {!r}".format(
condition))
p1 = Policy.query.filter_by(name=name).first()
if p1:
# The policy already exist, we need to update
if action is not None:
p1.action = action
if scope is not None:
p1.scope = scope
if realm is not None:
p1.realm = realm
if adminrealm is not None:
p1.adminrealm = adminrealm
if resolver is not None:
p1.resolver = resolver
if user is not None:
p1.user = user
if client is not None:
p1.client = client
if time is not None:
p1.time = time
if priority is not None:
p1.priority = priority
p1.active = active
p1.check_all_resolvers = check_all_resolvers
if conditions is not None:
p1.set_conditions(conditions)
save_config_timestamp()
db.session.commit()
ret = p1.id
else:
# Create a new policy
ret = Policy(name, action=action, scope=scope, realm=realm,
user=user, time=time, client=client, active=active,
resolver=resolver, adminrealm=adminrealm,
priority=priority,
check_all_resolvers=check_all_resolvers,
conditions=conditions).save()
return ret
[docs]@log_with(log)
def enable_policy(name, enable=True):
"""
Enable or disable the policy with the given name
:param name:
:return: ID of the policy
"""
if not Policy.query.filter(Policy.name == name).first():
raise ResourceNotFoundError(u"The policy with name '{0!s}' does not exist".format(name))
# Update the policy
p = set_policy(name=name, active=enable)
return p
[docs]@log_with(log)
def delete_policy(name):
"""
Function to delete one named policy.
Raise ResourceNotFoundError if there is no such policy.
:param name: the name of the policy to be deleted
:return: the ID of the deleted policy
:rtype: int
"""
return fetch_one_resource(Policy, name=name).delete()
[docs]@log_with(log)
def delete_all_policies():
policies = Policy.query.all()
for p in policies:
p.delete()
[docs]@log_with(log)
def export_policies(policies):
"""
This function takes a policy list and creates an export file from it
:param policies: a policy definition
:type policies: list of policy dictionaries
:return: the contents of the file
:rtype: string
"""
file_contents = ""
if policies:
for policy in policies:
file_contents += "[{0!s}]\n".format(policy.get("name"))
for key, value in policy.items():
file_contents += "{0!s} = {1!s}\n".format(key, value)
file_contents += "\n"
return file_contents
[docs]@log_with(log)
def import_policies(file_contents):
"""
This function imports policies from a file.
The file has a config_object format, i.e. the text file has a header
[<policy_name>]
key = value
and key value pairs.
:param file_contents: The contents of the file
:type file_contents: basestring
:return: number of imported policies
:rtype: int
"""
policies = ConfigObj(file_contents.split('\n'), encoding="UTF-8")
res = 0
for policy_name, policy in policies.items():
ret = set_policy(name=policy_name,
action=ast.literal_eval(policy.get("action")),
scope=policy.get("scope"),
realm=ast.literal_eval(policy.get("realm", "[]")),
user=ast.literal_eval(policy.get("user", "[]")),
resolver=ast.literal_eval(policy.get("resolver", "[]")),
client=ast.literal_eval(policy.get("client", "[]")),
time=policy.get("time", ""),
priority=policy.get("priority", "1")
)
if ret > 0:
log.debug("import policy {0!s}: {1!s}".format(policy_name, ret))
res += 1
return res
[docs]@log_with(log)
def get_static_policy_definitions(scope=None):
"""
These are the static hard coded policy definitions.
They can be enhanced by token based policy definitions, that can be found
in lib.token.get_dynamic_policy_definitions.
:param scope: Optional the scope of the policies
:type scope: basestring
:return: allowed scopes with allowed actions, the type of action and a
description.
:rtype: dict
"""
resolvers = list(get_resolver_list())
realms = list(get_realms())
smtpconfigs = [server.config.identifier for server in get_smtpservers()]
radiusconfigs = [radius.config.identifier for radius in
get_radiusservers()]
radiusconfigs.insert(0, "userstore")
# "type": allowed values str, bool, int
# "desc": description of this action
# "value": list of allowed values of this action, works with int and str. A
# dropdown box will be displayed
# "group": ment to be used for grouping actions for better finding
# "mainmenu": list of enabled Menus. If this action is set, this menu
# is visible in the WebUI
pol = {
SCOPE.REGISTER: {
ACTION.RESOLVER: {'type': 'str',
'value': resolvers,
'desc': _('Define in which resolver the user '
'should be registered.')},
ACTION.REALM: {'type': 'str',
'value': realms,
'desc': _('Define in which realm the user should '
'be registered.')},
ACTION.EMAILCONFIG: {'type': 'str',
'value': smtpconfigs,
'desc': _('The SMTP server configuration, '
'that should be used to send the '
'registration email.')},
ACTION.REQUIREDEMAIL: {'type': 'str',
'desc': _('Only users with this email '
'address are allowed to '
'register. This is a regular '
'expression.')},
ACTION.REGISTERBODY: {'type': 'text',
'desc': _("The body of the registration "
"email. Use '{regkey}' as tag"
"for the registration key.")}
},
SCOPE.ADMIN: {
ACTION.ENABLE: {'type': 'bool',
'desc': _('Admin is allowed to enable tokens.'),
'mainmenu': [MAIN_MENU.TOKENS],
'group': GROUP.TOKEN},
ACTION.DISABLE: {'type': 'bool',
'desc': _('Admin is allowed to disable tokens.'),
'mainmenu': [MAIN_MENU.TOKENS],
'group': GROUP.TOKEN},
ACTION.SET: {'type': 'bool',
'desc': _(
'Admin is allowed to set token properties.'),
'mainmenu': [MAIN_MENU.TOKENS],
'group': GROUP.TOKEN},
ACTION.SETPIN: {'type': 'bool',
'desc': _(
'Admin is allowed to set the OTP PIN of '
'tokens.'),
'mainmenu': [MAIN_MENU.TOKENS],
'group': GROUP.TOKEN},
ACTION.SETTOKENINFO: {'type': 'bool',
'desc': _('Admin is allowed to manually set and delete token info.'),
'mainmenu': [MAIN_MENU.TOKENS],
'group': GROUP.TOKEN},
ACTION.ENROLLPIN: {'type': 'bool',
"desc": _("Admin is allowed to set the OTP "
"PIN during enrollment."),
'mainmenu': [MAIN_MENU.TOKENS],
'group': GROUP.ENROLLMENT},
ACTION.RESYNC: {'type': 'bool',
'desc': _('Admin is allowed to resync tokens.'),
'mainmenu': [MAIN_MENU.TOKENS],
'group': GROUP.TOKEN},
ACTION.RESET: {'type': 'bool',
'desc': _(
'Admin is allowed to reset the Failcounter of '
'a token.'),
'mainmenu': [MAIN_MENU.TOKENS],
'group': GROUP.TOKEN},
ACTION.REVOKE: {'tpye': 'bool',
'desc': _("Admin is allowed to revoke a token"),
'mainmenu': [MAIN_MENU.TOKENS],
'group': GROUP.TOKEN},
ACTION.ASSIGN: {'type': 'bool',
'desc': _(
'Admin is allowed to assign a token to a '
'user.'),
'mainmenu': [MAIN_MENU.TOKENS, MAIN_MENU.USERS],
'group': GROUP.TOKEN},
ACTION.UNASSIGN: {'type': 'bool',
'desc': _(
'Admin is allowed to remove the token from '
'a user, '
'i.e. unassign a token.'),
'mainmenu': [MAIN_MENU.TOKENS],
'group': GROUP.TOKEN},
ACTION.IMPORT: {'type': 'bool',
'desc': _(
'Admin is allowed to import token files.'),
'mainmenu': [MAIN_MENU.TOKENS],
'group': GROUP.SYSTEM},
ACTION.DELETE: {'type': 'bool',
'desc': _(
'Admin is allowed to remove tokens from the '
'database.'),
'mainmenu': [MAIN_MENU.TOKENS],
'group': GROUP.TOKEN},
ACTION.USERLIST: {'type': 'bool',
'desc': _(
'Admin is allowed to view the list of the '
'users.'),
'mainmenu': [MAIN_MENU.USERS],
'group': GROUP.GENERAL},
ACTION.MACHINELIST: {'type': 'bool',
'desc': _('The Admin is allowed to list '
'the machines.'),
'mainmenu': [MAIN_MENU.MACHINES],
'group': GROUP.MACHINE},
ACTION.MACHINETOKENS: {'type': 'bool',
'desc': _('The Admin is allowed to attach '
'and detach tokens to '
'machines.'),
'mainmenu': [MAIN_MENU.TOKENS,
MAIN_MENU.MACHINES],
'group': GROUP.MACHINE},
ACTION.AUTHITEMS: {'type': 'bool',
'desc': _('The Admin is allowed to fetch '
'authentication items of tokens '
'assigned to machines.'),
'group': GROUP.GENERAL},
ACTION.TOKENREALMS: {'type': 'bool',
'desc': _('Admin is allowed to manage the '
'realms of a token.'),
'mainmenu': [MAIN_MENU.TOKENS],
'group': GROUP.TOKEN},
ACTION.TOKENLIST: {'type': 'bool',
'desc': _('Admin is allowed to list tokens.'),
'mainmenu': [MAIN_MENU.TOKENS],
'group': GROUP.TOKEN},
ACTION.GETSERIAL: {'type': 'bool',
'desc': _('Admin is allowed to retrieve a serial'
' for a given OTP value.'),
'mainmenu': [MAIN_MENU.TOKENS],
"group": GROUP.TOOLS},
ACTION.GETRANDOM: {'type': 'bool',
'desc': _('Admin is allowed to retrieve '
'random keys from privacyIDEA.'),
'group': GROUP.TOOLS},
ACTION.COPYTOKENPIN: {'type': 'bool',
'desc': _(
'Admin is allowed to copy the PIN of '
'one token '
'to another token.'),
"group": GROUP.TOOLS},
ACTION.COPYTOKENUSER: {'type': 'bool',
'desc': _(
'Admin is allowed to copy the assigned '
'user to another'
' token, i.e. assign a user ot '
'another token.'),
"group": GROUP.TOOLS},
ACTION.LOSTTOKEN: {'type': 'bool',
'desc': _('Admin is allowed to trigger the '
'lost token workflow.'),
'mainmenu': [MAIN_MENU.TOKENS],
'group': GROUP.TOOLS},
ACTION.SYSTEMWRITE: {'type': 'bool',
"desc": _("Admin is allowed to write and "
"modify the system configuration."),
"group": GROUP.SYSTEM,
'mainmenu': [MAIN_MENU.CONFIG]},
ACTION.SYSTEMDELETE: {'type': 'bool',
"desc": _("Admin is allowed to delete "
"keys in the system "
"configuration."),
"group": GROUP.SYSTEM,
'mainmenu': [MAIN_MENU.CONFIG]},
ACTION.SYSTEMREAD: {'type': 'bool',
"desc": _("Admin is allowed to read "
"basic system configuration."),
"group": GROUP.SYSTEM,
'mainmenu': [MAIN_MENU.CONFIG]},
ACTION.CONFIGDOCUMENTATION: {'type': 'bool',
'desc': _('Admin is allowed to '
'export a documentation '
'of the complete '
'configuration including '
'resolvers and realm.'),
'group': GROUP.SYSTEM,
'mainmenu': [MAIN_MENU.CONFIG]},
ACTION.POLICYWRITE: {'type': 'bool',
"desc": _("Admin is allowed to write and "
"modify the policies."),
"group": GROUP.SYSTEM,
'mainmenu': [MAIN_MENU.CONFIG]},
ACTION.POLICYDELETE: {'type': 'bool',
"desc": _("Admin is allowed to delete "
"policies."),
"group": GROUP.SYSTEM,
'mainmenu': [MAIN_MENU.CONFIG]},
ACTION.POLICYREAD: {'type': 'bool',
'desc': _("Admin is allowed to read policies."),
'group': GROUP.SYSTEM,
'mainmenu': [MAIN_MENU.CONFIG]},
ACTION.RESOLVERWRITE: {'type': 'bool',
"desc": _("Admin is allowed to write and "
"modify the "
"resolver and realm "
"configuration."),
"group": GROUP.SYSTEM,
'mainmenu': [MAIN_MENU.CONFIG]},
ACTION.RESOLVERDELETE: {'type': 'bool',
"desc": _("Admin is allowed to delete "
"resolvers and realms."),
"group": GROUP.SYSTEM,
'mainmenu': [MAIN_MENU.CONFIG]},
ACTION.RESOLVERREAD: {'type': 'bool',
'desc': _("Admin is allowed to read resolvers."),
'group': GROUP.SYSTEM,
' mainmenu': [MAIN_MENU.CONFIG]},
ACTION.CACONNECTORWRITE: {'type': 'bool',
"desc": _("Admin is allowed to create new"
" CA Connector definitions "
"and modify existing ones."),
"group": GROUP.SYSTEM,
'mainmenu': [MAIN_MENU.CONFIG]},
ACTION.CACONNECTORDELETE: {'type': 'bool',
"desc": _("Admin is allowed to delete "
"CA Connector definitions."),
"group": GROUP.SYSTEM,
'mainmenu': [MAIN_MENU.CONFIG]},
ACTION.MACHINERESOLVERWRITE: {'type': 'bool',
'desc': _("Admin is allowed to "
"write and modify the "
"machine resolvers."),
'group': GROUP.SYSTEM,
'mainmenu': [MAIN_MENU.CONFIG]},
ACTION.MACHINERESOLVERDELETE: {'type': 'bool',
'desc': _("Admin is allowed to "
"delete "
"machine resolvers."),
'group': GROUP.SYSTEM,
'mainmenu': [MAIN_MENU.CONFIG]},
ACTION.MACHINERESOLVERREAD: {'type': 'bool',
'desc': _("Admin is allowed to "
"read "
"machine resolvers."),
'group': GROUP.SYSTEM,
'mainmenu': [MAIN_MENU.CONFIG]},
ACTION.OTPPINMAXLEN: {'type': 'int',
'value': list(range(0, 32)),
"desc": _("Set the maximum allowed length "
"of the OTP PIN."),
'group': GROUP.PIN},
ACTION.OTPPINMINLEN: {'type': 'int',
'value': list(range(0, 32)),
"desc": _("Set the minimum required length "
"of the OTP PIN."),
'group': GROUP.PIN},
ACTION.OTPPINCONTENTS: {'type': 'str',
"desc": _("Specifiy the required "
"contents of the OTP PIN. "
"(c)haracters, (n)umeric, "
"(s)pecial, (o)thers. [+/-]!"),
'group': GROUP.PIN},
ACTION.AUDIT: {'type': 'bool',
"desc": _("Admin is allowed to view the Audit log."),
"group": GROUP.SYSTEM,
'mainmenu': [MAIN_MENU.AUDIT]},
ACTION.AUDIT_AGE: {'type': 'str',
"desc": _("The admin will only see audit "
"entries of the last 10d, 3m or 2y."),
"group": GROUP.SYSTEM,
'mainmenu': [MAIN_MENU.AUDIT]},
ACTION.AUDIT_DOWNLOAD: {'type': 'bool',
"desc": _("The admin is allowed to download "
"the complete auditlog."),
"group": GROUP.SYSTEM,
'mainmenu': [MAIN_MENU.AUDIT]},
ACTION.ADDUSER: {'type': 'bool',
"desc": _("Admin is allowed to add users in a "
"userstore/UserIdResolver."),
"group": GROUP.USER,
'mainmenu': [MAIN_MENU.USERS]},
ACTION.UPDATEUSER: {'type': 'bool',
"desc": _("Admin is allowed to update the "
"users data in a userstore."),
"group": GROUP.USER,
'mainmenu': [MAIN_MENU.USERS]},
ACTION.DELETEUSER: {'type': 'bool',
"desc": _("Admin is allowed to delete a user "
"object in a userstore."),
'mainmenu': [MAIN_MENU.USERS],
'group': GROUP.USER},
ACTION.SETHSM: {'type': 'bool',
'desc': _("Admin is allowed to set the password "
"of the HSM/Security Module."),
'group': GROUP.SYSTEM},
ACTION.GETCHALLENGES: {'type': 'bool',
'desc': _("Admin is allowed to retrieve "
"the list of active "
"challenges."),
'mainmenu': [MAIN_MENU.TOKENS],
'group': GROUP.GENERAL},
ACTION.SMTPSERVERWRITE: {'type': 'bool',
'desc': _("Admin is allowed to write new "
"SMTP server definitions."),
'mainmenu': [MAIN_MENU.CONFIG],
'group': GROUP.SYSTEM},
ACTION.SMTPSERVERREAD: {'type': 'bool',
'desc': _("Admin is allowed to read "
"SMTP server definitions."),
'mainmenu': [MAIN_MENU.CONFIG],
'group': GROUP.SYSTEM},
ACTION.RADIUSSERVERWRITE: {'type': 'bool',
'desc': _("Admin is allowed to write "
"new RADIUS server "
"definitions."),
'mainmenu': [MAIN_MENU.CONFIG],
'group': GROUP.SYSTEM},
ACTION.RADIUSSERVERREAD: {'type': 'bool',
'desc': _("Admin is allowed to read "
"RADIUS server definitions."),
'mainmenu': [MAIN_MENU.CONFIG],
'group': GROUP.SYSTEM},
ACTION.PRIVACYIDEASERVERWRITE: {'type': 'bool',
'desc': _("Admin is allowed to "
"write remote "
"privacyIDEA server "
"definitions."),
'mainmenu': [MAIN_MENU.CONFIG],
'group': GROUP.SYSTEM},
ACTION.PRIVACYIDEASERVERREAD: {'type': 'bool',
'desc': _("Admin is allowed to "
"read remote "
"privacyIDEA server "
"definitions."),
'mainmenu': [MAIN_MENU.CONFIG],
'group': GROUP.SYSTEM},
ACTION.PERIODICTASKWRITE: {'type': 'bool',
'desc': _("Admin is allowed to write "
"periodic task definitions."),
'mainmenu': [MAIN_MENU.CONFIG],
'group': GROUP.SYSTEM},
ACTION.PERIODICTASKREAD: {'type': 'bool',
'desc': _("Admin is allowed to read "
"periodic task definitions."),
'mainmenu': [MAIN_MENU.CONFIG],
'group': GROUP.SYSTEM},
ACTION.STATISTICSREAD: {'type': 'bool',
'desc': _("Admin is allowed to read statistics data."),
'group': GROUP.SYSTEM},
ACTION.STATISTICSDELETE: {'type': 'bool',
'desc': _("Admin is allowed to delete statistics data."),
'group': GROUP.SYSTEM},
ACTION.EVENTHANDLINGWRITE: {'type': 'bool',
'desc': _("Admin is allowed to write "
"and modify the event "
"handling configuration."),
'mainmenu': [MAIN_MENU.CONFIG],
'group': GROUP.SYSTEM},
ACTION.EVENTHANDLINGREAD: {'type': 'bool',
'desc': _("Admin is allowed to read event "
"handling configuration."),
'mainmenu': [MAIN_MENU.CONFIG],
'group': GROUP.SYSTEM},
ACTION.SMSGATEWAYWRITE: {'type': 'bool',
'desc': _("Admin is allowed to write "
"and modify SMS gateway "
"definitions."),
'mainmenu': [MAIN_MENU.CONFIG],
'group': GROUP.SYSTEM},
ACTION.SMSGATEWAYREAD: {'type': 'bool',
'desc': _("Admin is allowed to read "
"SMS gateway definitions."),
'mainmenu': [MAIN_MENU.CONFIG],
'group': GROUP.SYSTEM},
ACTION.CLIENTTYPE: {'type': 'bool',
'desc': _("Admin is allowed to get the list "
"of authenticated clients and their "
"types."),
'mainmenu': [MAIN_MENU.COMPONENTS],
'group': GROUP.SYSTEM},
ACTION.MANAGESUBSCRIPTION: {
'type': 'bool',
'desc': _("Admin is allowed to add and delete component "
"subscriptions."),
'mainmenu': [MAIN_MENU.COMPONENTS],
'group': GROUP.SYSTEM},
ACTION.TRIGGERCHALLENGE: {
'type': 'bool',
'desc': _("The Admin is allowed to trigger a challenge for "
"e.g. SMS OTP token."),
'mainmenu': [],
'group': GROUP.GENERAL
}
},
SCOPE.USER: {
ACTION.ASSIGN: {
'type': 'bool',
'desc': _("The user is allowed to assign an existing token"
" that is not yet assigned"
" using the token serial number."),
'mainmenu': [MAIN_MENU.TOKENS],
'group': GROUP.TOKEN},
ACTION.DISABLE: {'type': 'bool',
'desc': _(
'The user is allowed to disable his own '
'tokens.'),
'mainmenu': [MAIN_MENU.TOKENS],
'group': GROUP.TOKEN},
ACTION.ENABLE: {'type': 'bool',
'desc': _(
"The user is allowed to enable his own "
"tokens."),
'mainmenu': [MAIN_MENU.TOKENS],
'group': GROUP.TOKEN},
ACTION.DELETE: {'type': 'bool',
"desc": _(
"The user is allowed to delete his own "
"tokens."),
'mainmenu': [MAIN_MENU.TOKENS],
'group': GROUP.TOKEN},
ACTION.UNASSIGN: {'type': 'bool',
"desc": _("The user is allowed to unassign his "
"own tokens."),
'mainmenu': [MAIN_MENU.TOKENS],
'group': GROUP.TOKEN},
ACTION.RESYNC: {'type': 'bool',
"desc": _("The user is allowed to resyncronize his "
"tokens."),
'mainmenu': [MAIN_MENU.TOKENS],
'group': GROUP.TOKEN},
ACTION.REVOKE: {'type': 'bool',
'desc': _("The user is allowed to revoke a "
"token"),
'mainmenu': [MAIN_MENU.TOKENS],
'group': GROUP.TOKEN},
ACTION.RESET: {'type': 'bool',
'desc': _('The user is allowed to reset the '
'failcounter of his tokens.'),
'mainmenu': [MAIN_MENU.TOKENS],
'group': GROUP.TOKEN},
ACTION.SETPIN: {'type': 'bool',
"desc": _("The user is allowed to set the OTP "
"PIN of his tokens."),
'mainmenu': [MAIN_MENU.TOKENS],
'group': GROUP.PIN},
ACTION.SETDESCRIPTION: {'type': 'bool',
'desc': _('The user is allowed to set the token description.'),
'mainmenu': [MAIN_MENU.TOKENS],
'group': GROUP.TOKEN},
ACTION.ENROLLPIN: {'type': 'bool',
"desc": _("The user is allowed to set the OTP "
"PIN during enrollment."),
'group': GROUP.PIN},
ACTION.OTPPINMAXLEN: {'type': 'int',
'value': list(range(0, 32)),
"desc": _("Set the maximum allowed length "
"of the OTP PIN."),
'group': GROUP.PIN},
ACTION.OTPPINMINLEN: {'type': 'int',
'value': list(range(0, 32)),
"desc": _("Set the minimum required length "
"of the OTP PIN."),
'group': GROUP.PIN},
ACTION.OTPPINCONTENTS: {'type': 'str',
"desc": _("Specifiy the required "
"contents of the OTP PIN. "
"(c)haracters, (n)umeric, "
"(s)pecial, (o)thers. [+/-]!"),
'group': GROUP.PIN},
ACTION.AUDIT: {
'type': 'bool',
'desc': _('Allow the user to view his own token history.'),
'mainmenu': [MAIN_MENU.AUDIT]},
ACTION.AUDIT_AGE: {'type': 'str',
"desc": _("The user will only see audit "
"entries of the last 10d, 3m or 2y."),
'mainmenu': [MAIN_MENU.AUDIT]},
ACTION.USERLIST: {'type': 'bool',
'desc': _("The user is allowed to view his "
"own user information."),
'mainmenu': [MAIN_MENU.USERS]},
ACTION.UPDATEUSER: {'type': 'bool',
'desc': _("The user is allowed to update his "
"own user information, like changing "
"his password."),
'mainmenu': [MAIN_MENU.USERS]},
ACTION.PASSWORDRESET: {'type': 'bool',
'desc': _("The user is allowed to do a "
"password reset in an editable "
"UserIdResolver."),
'mainmenu': []}
},
SCOPE.ENROLL: {
ACTION.MAXTOKENREALM: {
'type': 'int',
'desc': _('Limit the number of allowed tokens in a realm.'),
'group': GROUP.TOKEN},
ACTION.MAXTOKENUSER: {
'type': 'int',
'desc': _('Limit the number of tokens a user may have '
'assigned.'),
'group': GROUP.TOKEN},
ACTION.MAXACTIVETOKENUSER: {
'type': 'int',
'desc': _('Limit the number of active tokens a user may have assigned.'),
'group': GROUP.TOKEN},
ACTION.OTPPINRANDOM: {
'type': 'int',
'value': list(range(0, 32)),
"desc": _("Set a random OTP PIN with this length for a "
"token."),
'group': GROUP.PIN},
ACTION.PINHANDLING: {
'type': 'str',
'desc': _('In case of a random OTP PIN use this python '
'module to process the PIN.'),
'group': GROUP.PIN},
ACTION.CHANGE_PIN_FIRST_USE: {
'type': 'bool',
'desc': _("If the administrator sets the OTP PIN during "
"enrollment or later, the user will have to change "
"the PIN during first use."),
'group': GROUP.PIN
},
ACTION.CHANGE_PIN_EVERY: {
'type': 'str',
'desc': _("The user needs to change his PIN on a regular "
"basis. To change the PIN every 180 days, "
"enter '180d'."),
'group': GROUP.PIN
},
ACTION.ENCRYPTPIN: {
'type': 'bool',
"desc": _("The OTP PIN can be hashed or encrypted. Hashing "
"the PIN is the default behaviour."),
'group': GROUP.PIN},
ACTION.TOKENLABEL: {
'type': 'str',
'desc': _("Set label for a new enrolled Google Authenticator. "
"Possible tags are <u> (user), <r> ("
"realm), <s> (serial)."),
'group': GROUP.TOKEN},
ACTION.TOKENISSUER: {
'type': 'str',
'desc': _("This is the issuer label for new enrolled Google "
"Authenticators."),
'group': GROUP.TOKEN
},
ACTION.APPIMAGEURL: {
'type': 'str',
'desc': _("This is the URL to the token image for smartphone apps "
"like FreeOTP."),
'group': GROUP.TOKEN
},
ACTION.AUTOASSIGN: {
'type': 'str',
'value': [AUTOASSIGNVALUE.NONE, AUTOASSIGNVALUE.USERSTORE],
'desc': _("Users can assign a token just by using the "
"unassigned token to authenticate."),
'group': GROUP.TOKEN},
ACTION.LOSTTOKENPWLEN: {
'type': 'int',
'value': list(range(1, 32)),
'desc': _('The length of the password in case of '
'temporary token (lost token).')},
ACTION.LOSTTOKENPWCONTENTS: {
'type': 'str',
'desc': _('The contents of the temporary password, '
'described by the characters C, c, n, s, 8.')},
ACTION.LOSTTOKENVALID: {
'type': 'int',
'value': list(range(1, 61)),
'desc': _('The length of the validity for the temporary '
'token (in days).')},
},
SCOPE.AUTH: {
ACTION.OTPPIN: {
'type': 'str',
'value': [ACTIONVALUE.TOKENPIN, ACTIONVALUE.USERSTORE,
ACTIONVALUE.NONE],
'desc': _('Either use the Token PIN , use the Userstore '
'Password or use no fixed password '
'component.')},
ACTION.CHALLENGERESPONSE: {
'type': 'str',
'desc': _('This is a whitespace separated list of tokentypes, '
'that can be used with challenge response.')
},
ACTION.CHALLENGETEXT: {
'type': 'str',
'desc': _('Use an alternate challenge text for telling the '
'user to enter an OTP value.')
},
ACTION.CHALLENGETEXT_HEADER: {
'type': 'str',
'desc': _("If there are several different challenges, this text precedes the list"
" of the challenge texts.")
},
ACTION.CHALLENGETEXT_FOOTER: {
'type': 'str',
'desc': _("If there are several different challenges, this text follows the list"
" of the challenge texts.")
},
ACTION.PASSTHRU: {
'type': 'str',
'value': radiusconfigs,
'desc': _('If set, the user in this realm will be '
'authenticated against the userstore or against the '
'given RADIUS config,'
' if the user has no tokens assigned.')
},
ACTION.PASSTHRU_ASSIGN: {
'type': 'str',
'desc': _('This allows to automatically assign a Token within privacyIDEA, if the '
'user was authenticated via passthru against a RADIUS server. The OTP value '
'is used to find the unassigned token in privacyIDEA. Enter the length of the OTP value '
'and where the PIN is set like 8:pin or pin:6.')
},
ACTION.PASSNOTOKEN: {
'type': 'bool',
'desc': _('If the user has no token, the authentication '
'request for this user will always be true.')
},
ACTION.PASSNOUSER: {
'type': 'bool',
'desc': _('If the user user does not exist, '
'the authentication request for this '
'non-existing user will always be true.')
},
ACTION.MANGLE: {
'type': 'str',
'desc': _('Can be used to modify the parameters pass, '
'user and realm in an authentication request. See '
'the documentation for an example.')
},
ACTION.RESETALLTOKENS: {
'type': 'bool',
'desc': _('If a user authenticates successfully reset the '
'failcounter of all of his tokens.')
},
ACTION.AUTH_CACHE: {
'type': 'str',
'desc': _('Cache the password used for authentication and '
'allow authentication with the same credentials for a '
'certain amount of time. '
'Specify timeout like 4h or 4h/5m.')
}
},
SCOPE.AUTHZ: {
ACTION.AUTHMAXSUCCESS: {
'type': 'str',
'desc': _("You can specify how many successful authentication "
"requests a user is allowed to do in a given time. "
"Specify like 1/5s, 2/10m, 10/1h - s, m, h being "
"second, minute and hour.")
},
ACTION.AUTHMAXFAIL: {
'type': 'str',
'desc': _("You can specify how many failed authentication "
"requests a user is allowed to do in a given time. "
"Specify like 1/5s, 2/10m, 10/1h - s, m, h being "
"second, minute and hour.")
},
ACTION.LASTAUTH: {
'type': 'str',
'desc': _("You can specify in which time frame the user needs "
"to authenticate again with this token. If the user "
"authenticates later, authentication will fail. "
"Specify like 30h, 7d or 1y.")
},
ACTION.TOKENTYPE: {
'type': 'str',
'desc': _('The user will only be authenticated with this '
'very tokentype.')},
ACTION.SERIAL: {
'type': 'str',
'desc': _('The user will only be authenticated if the serial '
'number of the token matches this regexp.')},
ACTION.TOKENINFO: {
'type': 'str',
'desc': _("The user will only be authenticated if the tokeninfo "
"field matches the regexp. key/<regexp>/")},
ACTION.SETREALM: {
'type': 'str',
'value': realms,
'desc': _('The Realm of the user is set to this very realm. '
'This is important if the user is not contained in '
'the default realm and can not pass his realm.')},
ACTION.NODETAILSUCCESS: {
'type': 'bool',
'desc': _('In case of successful authentication additional '
'no detail information will be returned.')},
ACTION.NODETAILFAIL: {
'type': 'bool',
'desc': _('In case of failed authentication additional '
'no detail information will be returned.')},
ACTION.ADDUSERINRESPONSE: {
'type': 'bool',
'desc': _('In case of successful authentication user data '
'will be added in the detail branch of the '
'authentication response.')},
ACTION.ADDRESOLVERINRESPONSE: {
'type': 'bool',
'desc': _('In case of successful authentication the user resolver and '
'realm will be added in the detail branch of the '
'authentication response.')},
ACTION.APIKEY: {
'type': 'bool',
'desc': _('The sending of an API Auth Key is required during'
'authentication. This avoids rogue authenticate '
'requests against the /validate/check interface.')
}
},
SCOPE.WEBUI: {
ACTION.LOGINMODE: {
'type': 'str',
'desc': _(
'If set to "privacyIDEA" the users and admins need to '
'authenticate against privacyIDEA when they log in '
'to the Web UI. Defaults to "userstore"'),
'value': [LOGINMODE.USERSTORE, LOGINMODE.PRIVACYIDEA,
LOGINMODE.DISABLE],
},
ACTION.LOGIN_TEXT: {
'type': 'str',
'desc': _('An alternative text to display on the WebUI login dialog instead of "Please sign in".')
},
ACTION.SEARCH_ON_ENTER: {
'type': 'bool',
'desc': _('When searching in the user list, the search will '
'only performed when pressing enter.')
},
ACTION.TIMEOUT_ACTION: {
'type': 'str',
'desc': _('The action taken when a user is idle '
'beyond the logout_time limit. '
'Defaults to "lockscreen".'),
'value': [TIMEOUT_ACTION.LOGOUT, TIMEOUT_ACTION.LOCKSCREEN],
},
ACTION.REMOTE_USER: {
'type': 'str',
'value': [REMOTE_USER.ACTIVE, REMOTE_USER.DISABLE],
'desc': _('The REMOTE_USER set by the webserver can be used '
'to login to privacyIDEA or it will be ignored. '
'Defaults to "disable".')
},
ACTION.LOGOUTTIME: {
'type': 'int',
'desc': _("Set the time in seconds after which the user will "
"be logged out from the WebUI. Default: 120")
},
ACTION.TOKENPAGESIZE: {
'type': 'int',
'desc': _("Set how many tokens should be displayed in the "
"token view on one page.")
},
ACTION.USERPAGESIZE: {
'type': 'int',
'desc': _("Set how many users should be displayed in the user "
"view on one page.")
},
ACTION.CUSTOM_MENU: {
'type': 'str',
'desc': _("Use your own html template for the web UI menu.")
},
ACTION.CUSTOM_BASELINE: {
'type': 'str',
'desc': _("Use your own html template for the web UI baseline/footer.")
},
ACTION.USERDETAILS: {
'type': 'bool',
'desc': _("Whether the user ID and the resolver should be "
"displayed in the token list.")
},
ACTION.POLICYTEMPLATEURL: {
'type': 'str',
'desc': _("The URL of a repository, where the policy "
"templates can be found. (Default "
"https: //raw.githubusercontent.com/ privacyidea/"
"policy-templates /master/templates/)")
},
ACTION.TOKENWIZARD: {
'type': 'bool',
'desc': _("As long as a user has no token, he will only see"
" a token wizard in the UI.")
},
ACTION.TOKENWIZARD2ND: {
'type': 'bool',
'desc': _("The tokenwizard will be displayed in the token "
"menu, even if the user already has a token.")
},
ACTION.DIALOG_NO_TOKEN: {
'type': 'bool',
'desc': _("The welcome dialog will be displayed if the user has no tokens assigned.")
},
ACTION.DEFAULT_TOKENTYPE: {
'type': 'str',
'desc': _("This is the default token type in the token "
"enrollment dialog."),
'value': get_token_types()
},
ACTION.REALMDROPDOWN: {
'type': 'str',
'desc': _("A list of realm names, which are "
"displayed in a drop down menu in the WebUI login "
"screen. Realms are separated by white spaces.")
},
ACTION.HIDE_WELCOME: {
'type': 'bool',
'desc': _("If this checked, the administrator will not see "
"the welcome dialog anymore.")
},
ACTION.HIDE_BUTTONS: {
'type': 'bool',
'desc': _("Per default disabled actions result in disabled buttons. When"
" checking this action, buttons of disabled actions are hidden.")
},
ACTION.SHOW_SEED: {
'type': 'bool',
'desc': _("If this is checked, the seed "
"will be displayed as text during enrollment.")
}
}
}
if scope:
ret = pol.get(scope, {})
else:
ret = pol
return ret
[docs]def get_action_values_from_options(scope, action, options):
"""
This function is used in the library level to fetch policy action values
from a given option dictionary.
:return: A scalar, string or None
"""
value = None
g = options.get("g")
if g:
user_object = options.get("user")
username = None
realm = None
if user_object:
username = user_object.login
realm = user_object.realm
clientip = options.get("clientip")
policy_object = g.policy_object
value = policy_object. \
get_action_values(action=action,
scope=scope,
realm=realm,
user=username,
client=clientip,
unique=True,
allow_white_space_in_action=True)
if len(value) >= 1:
return list(value)[0]
else:
return None
return value
[docs]def get_policy_condition_sections():
"""
:return: a dictionary mapping condition sections to dictionaries with the following keys:
* ``"description"``, a human-readable description of the section
"""
return {
CONDITION_SECTION.USERINFO: {
"description": _("The policy only matches if certain conditions on the user info are fulfilled.")
}
}
[docs]def get_policy_condition_comparators():
"""
:return: a dictionary mapping comparators to dictionaries with the following keys:
* ``"description"``, a human-readable description of the comparator
"""
return {comparator: {"description": description}
for comparator, description in COMPARATOR_DESCRIPTIONS.items()}