# Copyright 2012 The Chromium Authors # Use of this source code is governed by a BSD-style license that can be # found in the LICENSE file. # If this presubmit check fails or misbehaves, please complain to # chromium-policy-owners@google.com. PRESUBMIT_VERSION = '2.0.0' import glob import os import sys from xml.dom import minidom from xml.parsers import expat sys.path.append(os.path.abspath('./resources')) from policy_templates import GetPolicyTemplates sys.path.append(os.path.join('..', '..', 'third_party')) import pyyaml _CACHED_FILES = {} _CACHED_POLICY_CHANGE_LIST = [] _CACHED_POLICY_DEFINITION_MAP = {} _COMPONENTS_POLICY_PATH = os.path.join('components', 'policy') _TEST_CASES_DEPOT_PATH = os.path.join( _COMPONENTS_POLICY_PATH, 'test' , 'data', 'pref_mapping') _PRESUBMIT_PATH = os.path.join(_COMPONENTS_POLICY_PATH, 'PRESUBMIT.py') _TOOLS_PATH = os.path.join(_COMPONENTS_POLICY_PATH, 'tools') _SYNTAX_CHECK_SCRIPT_PATH = os.path.join(_TOOLS_PATH, 'syntax_check_policy_template_json.py') _TEMPLATES_PATH = os.path.join(_COMPONENTS_POLICY_PATH, 'resources', 'templates') _MESSAGES_PATH = os.path.join(_TEMPLATES_PATH, 'messages.yaml') _COMMON_SCHEMAS_PATH = os.path.join(_TEMPLATES_PATH, 'common_schemas.yaml') _POLICIES_DEFINITIONS_PATH = os.path.join(_TEMPLATES_PATH, 'policy_definitions') _POLICIES_YAML_PATH = os.path.join(_TEMPLATES_PATH, 'policies.yaml') _HISTOGRAMS_PATH = os.path.join( 'tools', 'metrics', 'histograms', 'metadata', 'enterprise', 'enums.xml') _DEVICE_POLICY_PROTO_PATH = os.path.join( _COMPONENTS_POLICY_PATH, 'proto', 'chrome_device_policy.proto') _DEVICE_POLICY_PROTO_MAP_PATH = os.path.join( _TEMPLATES_PATH, 'device_policy_proto_map.yaml') _LEGACY_DEVICE_POLICY_PROTO_MAP_PATH = os.path.join( _TEMPLATES_PATH, 'legacy_device_policy_proto_map.yaml') # 100 MiB upper limit on the total device policy external data max size limits # due to the security reasons. # You can increase this limit if you're introducing new external data type # device policy, but be aware that too heavy policies could result in user # profiles not having enough space on the device. TOTAL_DEVICE_POLICY_EXTERNAL_DATA_MAX_SIZE = 1024 * 1024 * 100 def _SafeListDir(directory): '''Wrapper around os.listdir() that ignores files created by Finder.app.''' # On macOS, Finder.app creates .DS_Store files when a user visit a # directory causing failure of the script laters on because there # are no such group as .DS_Store. Skip the file to prevent the error. return filter(lambda name:(name != '.DS_Store'),os.listdir(directory)) def _SkipPresubmitChecks(input_api, files_watchlist): '''Returns True if no file or file under the directories specified was affected in this change. Args: input_api files_watchlist: List of files or directories ''' for file in files_watchlist: if any(os.path.commonpath([file, f.LocalPath()]) == file for f in input_api.change.AffectedFiles()): return False return True def _CheckerWasModified(input_api): '''Returns True if the syntax checker file was modified. Args: input_api ''' return any(_SYNTAX_CHECK_SCRIPT_PATH == f for f in input_api.change.LocalPaths()) def _LoadYamlFile(root, path): str_path = str(path) if str_path not in _CACHED_FILES: with open(os.path.join(root, path), encoding='utf-8') as f: _CACHED_FILES[str_path] = pyyaml.safe_load(f) return _CACHED_FILES[str_path] def _GetKnownFeatures(input_api): feature_messages = [] root = input_api.change.RepositoryRoot() messages = _LoadYamlFile(root, _MESSAGES_PATH) for message in messages: if message.startswith('doc_feature_'): feature_messages.append(message[12:]) return feature_messages def _GetCommonSchema(input_api): root = input_api.change.RepositoryRoot() commmon_schemas = _LoadYamlFile(root, _COMMON_SCHEMAS_PATH) return commmon_schemas def _GetCurrentVersion(input_api): if 'version' in _CACHED_FILES: return _CACHED_FILES['version'] try: root = input_api.change.RepositoryRoot() version_path = input_api.os_path.join(root, 'chrome', 'VERSION') with open(version_path, "rb") as f: _CACHED_FILES['version'] = int(f.readline().split(b"=")[1]) except: pass return _CACHED_FILES['version'] def _GetPolicyDefinitionMap(input_api): '''Returns a dict of policy definitions as they are in this changelist. Args: input_api Returns: Dictionary of policies loaded from their yaml files with the policy name as the key. ''' global _CACHED_POLICY_DEFINITION_MAP if not _CACHED_POLICY_DEFINITION_MAP: policy_definitions = GetPolicyTemplates()['policy_definitions'] _CACHED_POLICY_DEFINITION_MAP = \ {policy['name']: policy for policy in policy_definitions} return _CACHED_POLICY_DEFINITION_MAP def _GetUnchangedPolicyList(input_api): '''Returns a list of policies NOT modified in the changelist Args: input_api Returns: The list of policies loaded from their yaml files with the 'name' added. ''' changed_policy_names = { policy['policy'] for policy in _GetPolicyChangeList(input_api) } root = input_api.change.RepositoryRoot() policies_dir = input_api.os_path.join(root, _POLICIES_DEFINITIONS_PATH) results = [] for path in glob.iglob(policies_dir + '/**/*.yaml', recursive=True): filename = os.path.basename(path) if not filename.endswith(".yaml"): continue; if (filename == '.group.details.yaml' or filename == 'policy_atomic_groups.yaml'): continue policy_name = filename.partition('.')[0] if policy_name in changed_policy_names: continue; policy = _LoadYamlFile('/', path) policy['name'] = policy_name results.append(policy) return results def _GetPolicyChangeList(input_api): '''Returns a list of policies modified in the changelist with their old schema next to their new schemas. Args: input_api Returns: List of objects with the following schema: { 'name': 'string', 'old_policy': dict, 'new_policy': dict } The policies are the values loaded from their yaml files. ''' if _CACHED_POLICY_CHANGE_LIST: return _CACHED_POLICY_CHANGE_LIST policy_changes_map = {} root = input_api.change.RepositoryRoot() policies_dir = input_api.os_path.join(root, _POLICIES_DEFINITIONS_PATH) policy_name_to_id = {name: id for id, name in _LoadYamlFile(root, _POLICIES_YAML_PATH)['policies'].items()} template_affected_files = [f for f in input_api.change.AffectedFiles() if os.path.commonpath([policies_dir, f.AbsoluteLocalPath()]) == policies_dir] for affected_file in template_affected_files: path = affected_file.AbsoluteLocalPath() filename = os.path.basename(path) policy_name = os.path.splitext(filename)[0] if (filename == '.group.details.yaml' or filename == 'policy_atomic_groups.yaml' or filename == 'OWNERS' or filename == 'DIR_METADATA'): continue old_policy = None new_policy = None if affected_file.Action() == 'M': old_policy = pyyaml.safe_load('\n'.join(affected_file.OldContents())) old_policy['name'] = policy_name old_policy['id'] = policy_name_to_id[policy_name] if affected_file.Action() == 'D': old_policy = pyyaml.safe_load('\n'.join(affected_file.OldContents())) old_policy['name'] = policy_name if affected_file.Action() != 'D': new_policy = pyyaml.safe_load('\n'.join(affected_file.NewContents())) new_policy['name'] = policy_name new_policy['id'] = policy_name_to_id[policy_name] # If a policy has been moved, it will appear as deleted then added. # Here we reconcile such policies so that a moved policy does not appear as # deleted. This also allows to verify the new policy schema against the one # from the previous location. if policy_name in policy_changes_map: # We previously found the policy at the new location, update old_policy # with the value from the old location. if policy_changes_map[policy_name]['old_policy'] == None: policy_changes_map[policy_name]['old_policy'] = old_policy # We previously found the policy at the old location, update new_policy # with the value from the new location. if policy_changes_map[policy_name]['new_policy'] == None: policy_changes_map[policy_name]['new_policy'] = new_policy else: policy_changes_map[policy_name] = { 'policy': policy_name, 'old_policy': old_policy, 'new_policy': new_policy} for policy_change in policy_changes_map.values(): _CACHED_POLICY_CHANGE_LIST.append(policy_change) return _CACHED_POLICY_CHANGE_LIST def _IsPolicyUnsupported(input_api, policy): '''Returns true if `policy` is unsupported on the current Chrome version on all platforms. These policies may not have any prefs and tests associated with them.''' if len(policy.get('future_on', [])) > 0: # If the policy will be released in the future, it is supported. return False current_version = _GetCurrentVersion(input_api) policy_platforms = _GetPlatformSupportMap(policy) for _, supported_versions in policy_platforms.items(): if not supported_versions['to']: # Policy doesn't have an end of support version. return False if supported_versions['to'] >= current_version: return False return True def CheckPolicyTestCases(input_api, output_api): '''Verifies that the all defined policies have a test case. This is ran when policy_test_cases.json, policies.yaml or this PRESUBMIT.py file are modified. ''' results = [] if _SkipPresubmitChecks( input_api, [_TEST_CASES_DEPOT_PATH, _POLICIES_YAML_PATH, _POLICIES_DEFINITIONS_PATH, _PRESUBMIT_PATH]): return results root = input_api.change.RepositoryRoot() # Gather expected test files policies_yaml = _LoadYamlFile(root, _POLICIES_YAML_PATH) policies = policies_yaml['policies'] policy_names = set(name for name in policies.values() if name) test_case_depot_path = os.path.join( root, _TEST_CASES_DEPOT_PATH) # Gather actual test files tested_policies = set() for file in _SafeListDir(test_case_depot_path): filename = os.fsdecode(file) policy_name = os.path.splitext(filename)[0] tested_policies.add(policy_name) # Finally check if any policies or tests are missing. policies_with_missing_tests = policy_names - tested_policies extra = tested_policies - policy_names error_missing = ("Policy '%s' is declared but its test file '%s' was not " "found. Please update the test accordingly.") error_extra = ("Policy '%s' is tested at '%s' but its policy definition was " "not found. Please update the policy definition accordingly.") results = [] for policy in policies_with_missing_tests: policy_definition = _GetPolicyDefinitionMap(input_api).get(policy, {}) if _IsPolicyUnsupported(input_api, policy_definition): # Unsupported policies won't have tests. continue results.append(output_api.PresubmitError( error_missing % ( policy, os.path.join(test_case_depot_path, f'{policy}.json')))) for policy in extra: results.append(output_api.PresubmitError( error_extra % ( policy, os.path.join(test_case_depot_path, f'{policy}.json')))) results.extend( input_api.canned_checks.CheckChangeHasNoTabs( input_api, output_api, source_file_filter=lambda x: x.LocalPath() == _TEST_CASES_DEPOT_PATH)) return results def CheckPolicyHistograms(input_api, output_api): results = [] if _SkipPresubmitChecks( input_api, [_HISTOGRAMS_PATH, _POLICIES_YAML_PATH, _PRESUBMIT_PATH]): return results root = input_api.change.RepositoryRoot() with open(os.path.join(root, _HISTOGRAMS_PATH), encoding='utf-8') as f: tree = minidom.parseString(f.read()) enums = (tree.getElementsByTagName('histogram-configuration')[0] .getElementsByTagName('enums')[0] .getElementsByTagName('enum')) policy_enum = [e for e in enums if e.getAttribute('name') == 'EnterprisePolicies'][0] policy_enum_ids = frozenset(int(e.getAttribute('value')) for e in policy_enum.getElementsByTagName('int')) policies_yaml = _LoadYamlFile(root, _POLICIES_YAML_PATH) policies = policies_yaml['policies'] policy_ids = frozenset([id for id, name in policies.items() if name]) missing_ids = policy_ids - policy_enum_ids extra_ids = policy_enum_ids - policy_ids error_missing = ("Policy '%s' (id %d) was added to " "policy_templates.json but not to " "src/tools/metrics/histograms/enums.xml. Please update " "both files. To regenerate the policy part of enums.xml, " "run:\n" "python tools/metrics/histograms/update_policies.py") error_extra = ("Policy id %d was found in " "src/tools/metrics/histograms/enums.xml, but no policy with " "this id exists in policy_templates.json. To regenerate the " "policy part of enums.xml, run:\n" "python tools/metrics/histograms/update_policies.py") results = [] for policy_id in missing_ids: results.append( output_api.PresubmitError(error_missing % (policies[policy_id], policy_id))) for policy_id in extra_ids: results.append(output_api.PresubmitError(error_extra % policy_id)) return results def CheckPolicyAtomicGroupsHistograms(input_api, output_api): '''Verifies that the all policy atomic groups have a histogram entry. This is ran when policies.yaml, tools/metrics/histograms/enums.xml or this PRESUBMIT.py file are modified. ''' results = [] if _SkipPresubmitChecks( input_api, [_HISTOGRAMS_PATH, _POLICIES_YAML_PATH, _PRESUBMIT_PATH]): return results root = input_api.change.RepositoryRoot() with open(os.path.join(root, _HISTOGRAMS_PATH), encoding='utf-8') as f: tree = minidom.parseString(f.read()) enums = (tree.getElementsByTagName('histogram-configuration')[0] .getElementsByTagName('enums')[0] .getElementsByTagName('enum')) atomic_group_enums = [e for e in enums if e.getAttribute('name') == 'PolicyAtomicGroups'] if not atomic_group_enums: return results atomic_group_enum = atomic_group_enums[0] atomic_group_enum_ids = frozenset(int(e.getAttribute('value')) for e in atomic_group_enum .getElementsByTagName('int')) policies_yaml = _LoadYamlFile(root, _POLICIES_YAML_PATH) atomic_groups = policies_yaml['atomic_groups'] atomic_group_ids = frozenset( [id for id, name in atomic_groups.items() if name]) missing_ids = atomic_group_ids - atomic_group_enum_ids extra_ids = atomic_group_enum_ids - atomic_group_ids error_missing = ("Policy atomic group '%s' (id %d) was added to " "policy_templates.json but not to " "src/tools/metrics/histograms/enums.xml. Please update " "both files. To regenerate the policy part of enums.xml, " "run:\n" "python tools/metrics/histograms/update_policies.py") error_extra = ("Policy atomic group id %d was found in " "src/tools/metrics/histograms/enums.xml, but no policy with " "this id exists in policy_templates.json. To regenerate the " "policy part of enums.xml, run:\n" "python tools/metrics/histograms/update_policies.py") results = [] for atomic_group_id in missing_ids: results.append(output_api.PresubmitError(error_missing % (atomic_groups[atomic_group_id], atomic_group_id))) for atomic_group_id in extra_ids: results.append(output_api.PresubmitError(error_extra % atomic_group_id)) return results def CheckMessages(input_api, output_api): '''Verifies that the all the messages from messages.yaml have the following format: {[key: string]: {text: string, desc: string}}. This is ran when messages.yaml or this PRESUBMIT.py file are modified. ''' results = [] if _SkipPresubmitChecks( input_api, [_MESSAGES_PATH, _PRESUBMIT_PATH]): return results root = input_api.change.RepositoryRoot() messages = _LoadYamlFile(root, _MESSAGES_PATH) for message in messages: # |key| must be a string, |value| a dict. if not isinstance(message, str): results.append( output_api.PresubmitError( f'Each message key must be a string, invalid key {message}')) continue if not isinstance(messages[message], dict): results.append( output_api.PresubmitError( f'Each message must be a dictionary, invalid message {message}')) continue if ('desc' not in messages[message] or not isinstance(messages[message]['desc'], str)): results.append( output_api.PresubmitError( f"'desc' string key missing in message {message}")) if ('text' not in messages[message] or not isinstance(messages[message]['text'], str)): results.append( output_api.PresubmitError( f"'text' string key missing in message {message}")) # There should not be any unknown keys in |value|. for vkey in messages[message]: if vkey not in ('desc', 'text'): results.append(output_api.PresubmitError( f'In message {message}: Unknown key: {vkey}')) return results def CheckMissingPlaceholders(input_api, output_api): '''Verifies that the all the messages from messages.yaml, caption and descriptions from files under templates/policy_definitions do not have malformed placeholders. This is ran when messages.yaml, files under templates/policy_definitions or this PRESUBMIT.py file are modified. ''' results = [] if _SkipPresubmitChecks( input_api, [_MESSAGES_PATH, _POLICIES_DEFINITIONS_PATH, _PRESUBMIT_PATH]): return results root = input_api.change.RepositoryRoot() new_policies = [change['new_policy'] for change in _GetPolicyChangeList(input_api)] messages = _LoadYamlFile(root, _MESSAGES_PATH) items = new_policies + list(messages.values()) for item in items: for key in ['desc', 'text']: if item is None: continue if not key in item: continue try: node = minidom.parseString(u'%s' % item[key]).childNodes[0] except expat.ExpatError as e: error = ( 'Error when checking for missing placeholders: %s in:\n' '!!\n%s\n!' % (e, item[key])) results.append(output_api.PresubmitError(error)) continue for child in node.childNodes: if child.nodeType == minidom.Node.TEXT_NODE and '$' in child.data: warning = ("Character '$' found outside of a placeholder in '%s'. " "Should it be in a placeholder ?") % item[key] results.append(output_api.PresubmitPromptWarning(warning)) return results def CheckDevicePolicyProtos(input_api, output_api): results = [] if _SkipPresubmitChecks( input_api, [_DEVICE_POLICY_PROTO_PATH, _DEVICE_POLICY_PROTO_MAP_PATH, _LEGACY_DEVICE_POLICY_PROTO_MAP_PATH, _PRESUBMIT_PATH]): return results root = input_api.change.RepositoryRoot() proto_map = _LoadYamlFile(root, _DEVICE_POLICY_PROTO_MAP_PATH) legacy_proto_map = _LoadYamlFile(root, _LEGACY_DEVICE_POLICY_PROTO_MAP_PATH) with open(os.path.join(root, _DEVICE_POLICY_PROTO_PATH), 'r', encoding='utf-8') as file: protos = file.read() results = [] # Check that proto_map does not have duplicate values. proto_paths = set() for proto_path in proto_map.values(): if proto_path in proto_paths: results.append(output_api.PresubmitError( f'Duplicate proto path {proto_path} in ' f'{os.path.basename(_DEVICE_POLICY_PROTO_MAP_PATH)}. ' 'Did you set the right path for your device policy?')) proto_paths.add(proto_path) # Check that legacy_proto_map does not have duplicate values. for proto_path_list in legacy_proto_map.values(): for proto_path in proto_path_list: if not proto_path: continue if proto_path in proto_paths: results.append(output_api.PresubmitError( f'Duplicate proto path {proto_path} in ' 'legacy_device_policy_proto_map.yaml.' 'Did you set the right path for your device policy?')) proto_paths.add(proto_path) for policy, proto_path in proto_map.items(): fields = proto_path.split(".") for field in fields: if field not in protos: results.append(output_api.PresubmitError( f"Policy '{policy}': Expected field '{field}' not found in " "chrome_device_policy.proto.")) return results def _GetPlatformSupportMap(policy): '''Returns a map of platforms to their support version range as an object with the keys `from` and `to`.''' platforms_and_versions = {} if not policy: return platforms_and_versions for supported_on in policy.get('supported_on', []): platform, versions = supported_on.split(':') supported_from, supported_to = versions.split('-') version_range = { 'from': int(supported_from) if supported_from else None, 'to': int(supported_to) if supported_to else None } if platform == 'chrome.*': for p in ['chrome.win', 'chrome.mac', 'chrome.linux']: platforms_and_versions[p] = version_range else: platforms_and_versions[platform] = version_range return platforms_and_versions def CheckPolicyChangeVersionPlatformCompatibility(input_api, output_api): '''Cheks if the modified policies are compatible with their previous version if any and if they are compatible with the current version. Args: policy_changelist: A list of changed policy definitions with their old and new values. original_file_contents: The full contents of the original policy templates file. current_version: The current major version of the branch as stored in chrome/VERSION.''' results = [] if _SkipPresubmitChecks( input_api, [_POLICIES_DEFINITIONS_PATH, _PRESUBMIT_PATH]): return results skip_compatibility_check = ('BYPASS_POLICY_COMPATIBILITY_CHECK' in input_api.change.tags) if skip_compatibility_check: return results policy_changelist = _GetPolicyChangeList(input_api) current_version = _GetCurrentVersion(input_api) for policy_changes in policy_changelist: original_policy = policy_changes['old_policy'] new_policy = policy_changes['new_policy'] policy_name = policy_changes['policy'] original_policy_platforms = _GetPlatformSupportMap(original_policy) new_policy_platforms = _GetPlatformSupportMap(new_policy) for platform, original_range in original_policy_platforms.items(): # Policy supported if original_range['from'] < current_version: if platform not in new_policy_platforms: results.append(output_api.PresubmitError( f"In policy {policy_name}: Policy has been removed on {platform}. " "A released policy cannot be removed. Mark it as deprecated and " "update the supported versions.")) if original_range['from'] >= current_version: if platform not in new_policy_platforms: results.append(output_api.PresubmitPromptWarning( f"Unreleased policy {policy_name} has been removed on {platform}.")) for platform, _ in new_policy_platforms.items(): new_from_version = new_policy_platforms[platform]['from'] if (new_from_version < current_version - 1 and platform not in original_policy_platforms): results.append(output_api.PresubmitError( f"In policy {policy_name}: Support can't be added on platform " f"{platform} because version {new_from_version} is already released.") ) if (new_from_version == current_version - 1 and platform not in original_policy_platforms): results.append(output_api.PresubmitPromptWarning( f"In policy {policy_name}: Support will be added on platform " f"{platform} version {new_from_version} which has already passed " "branch point. Please merge this change in Beta.")) if not new_policy_platforms[platform]['to']: continue # These warnings fire inappropriately in presubmit --all/--files runs, so # disable them in these cases to reduce the noise. if input_api.no_diffs: continue # Support for policies can only be removed for past version until we have # a better reminder process to cleanup the code related to deprecated # policies. if new_policy_platforms[platform]['to'] > current_version: previous_version = int(current_version) - 1 results.append(output_api.PresubmitPromptWarning( f"In policy {policy_name}: Support on platform {platform} can only " f"be removed for version {previous_version}. Please remove all " "references in the code to that policy since it will not be " f"supported in the current version {current_version}.")) return results def CheckMissingPolicyNames(input_api, output_api): results = [] if _SkipPresubmitChecks( input_api, [_MESSAGES_PATH, _POLICIES_DEFINITIONS_PATH, _SYNTAX_CHECK_SCRIPT_PATH, _PRESUBMIT_PATH]): return results root = input_api.change.RepositoryRoot() # Check for missing policy names in policy.yaml and policy names to be removed # from policy.yaml. policies_yaml = _LoadYamlFile(root, _POLICIES_YAML_PATH) policies = policies_yaml['policies'] policy_names = frozenset([name for _, name in policies.items() if name]) policy_changelist = _GetPolicyChangeList(input_api) for policy_change in policy_changelist: policy_name = policy_change['policy'] if policy_change['new_policy'] and policy_name not in policy_names: results.append(output_api.PresubmitError( f'{policy_name} needs an ID in {_POLICIES_YAML_PATH}')) if not policy_change['new_policy'] and policy_name in policy_names: results.append(output_api.PresubmitError( f'{policy_name}\'s needs to be erased from {_POLICIES_YAML_PATH}')) return results def CheckPoliciesYamlOrdering(input_api, output_api): results = [] if _SkipPresubmitChecks( input_api, [_POLICIES_YAML_PATH, _PRESUBMIT_PATH]): return results root = input_api.change.RepositoryRoot() with open(os.path.join(root, _POLICIES_YAML_PATH), 'r', encoding='utf-8') as f: policies_yaml_lines = f.readlines() previous_id = 0 error_msg_template = '' for line in policies_yaml_lines: if line.startswith(' '): if not error_msg_template: results.append(output_api.PresubmitError( f'Invalid syntax, missing either policies, or atomic_groups key.')) continue id = int(line.strip().split(':')[0]) if previous_id + 1 != id: results.append(output_api.PresubmitError(error_msg_template % id)) previous_id = id elif 'policies:' in line: error_msg_template = 'Policy ID %s is out of place' previous_id = 0 elif 'atomic_groups:' in line: error_msg_template = 'Atomic policy group ID %s is out of place' previous_id = 0 return results def CheckPolicyIds(input_api, output_api): results = [] if _SkipPresubmitChecks( input_api, [_MESSAGES_PATH, _POLICIES_DEFINITIONS_PATH, _SYNTAX_CHECK_SCRIPT_PATH, _PRESUBMIT_PATH]): return results root = input_api.change.RepositoryRoot() # Check for duplicated ids policies_yaml = _LoadYamlFile(root, _POLICIES_YAML_PATH) policies = policies_yaml['policies'] policy_ids = set() duplicated_policy_ids = [] for id, _ in policies.items(): if id in policy_ids: duplicated_policy_ids.add(id) policy_ids.add(id) if duplicated_policy_ids: duplicated_policy_ids_str = ', '.join(duplicated_policy_ids) results.append(output_api.PresubmitError( f'Duplicate ids {duplicated_policy_ids_str} in {_POLICIES_YAML_PATH}')) # Check for missing ids missing_ids = sorted(list(set(range(1, max(policy_ids) + 1)) - policy_ids)) if missing_ids: missing_ids_str = ', '.join(str(id) for id in missing_ids) results.append(output_api.PresubmitError( f'Missing policy ids {missing_ids_str} in {_POLICIES_YAML_PATH}')) return results def CheckPolicyDefinitions(input_api, output_api): results = [] if _SkipPresubmitChecks( input_api, [_MESSAGES_PATH, _POLICIES_DEFINITIONS_PATH, _SYNTAX_CHECK_SCRIPT_PATH, _COMMON_SCHEMAS_PATH, _PRESUBMIT_PATH]): return results # Get the current version from the VERSION file so that we can check # which policies are un-released and thus can be changed at will. current_version = _GetCurrentVersion(input_api) old_sys_path = sys.path tools_path = input_api.os_path.normpath(input_api.os_path.join( input_api.PresubmitLocalPath(), 'tools')) sys.path.append(tools_path) # Optimization: only load this when it's needed. import syntax_check_policy_template_json sys.path = old_sys_path schemas_by_id = _GetCommonSchema(input_api) checker = syntax_check_policy_template_json.PolicyTemplateChecker() checker.SetFeatures(_GetKnownFeatures(input_api)) # Check if there is a tag that allows us to bypass compatibility checks. # This can be used in situations where there is a bug in the validation # code or if a policy change needs to urgently be submitted. skip_compatibility_check = ('BYPASS_POLICY_COMPATIBILITY_CHECK' in input_api.change.tags) checker.CheckModifiedPolicies( _GetPolicyChangeList(input_api), current_version, schemas_by_id, skip_compatibility_check) if _CheckerWasModified(input_api): # Check the rest of the policies checker.CheckPolicyDefinitions(_GetUnchangedPolicyList(input_api), current_version, schemas_by_id) errors, warnings = checker.errors, checker.warnings # PRESUBMIT won't print warning if there is any error. Append warnings to # error for policy_templates.json so that they can always be printed # together. if errors: error_msgs = "\n".join(errors+warnings) return [output_api.PresubmitError('Syntax error(s) in file:', [_TEMPLATES_PATH], error_msgs)] elif warnings: warning_msgs = "\n".join(warnings) return [output_api.PresubmitPromptWarning('Syntax warning(s) in file:', [_TEMPLATES_PATH], warning_msgs)] return [] def CheckDevicePolicies(input_api, output_api): results = [] if _SkipPresubmitChecks( input_api, [_POLICIES_DEFINITIONS_PATH, _PRESUBMIT_PATH]): return results root = input_api.change.RepositoryRoot() policy_changelist = _GetPolicyChangeList(input_api) if not any(policy_change['new_policy'].get('device_only', False) or policy_change['new_policy']['type'] == 'external' for policy_change in policy_changelist if policy_change['new_policy'] != None): return results policy_definitions = list(_GetPolicyDefinitionMap(input_api).values()) proto_map = _LoadYamlFile(root, _DEVICE_POLICY_PROTO_MAP_PATH) legacy_proto_map = _LoadYamlFile(root, _LEGACY_DEVICE_POLICY_PROTO_MAP_PATH) # Check policy did not change its device_only value for policy_change in policy_changelist: old_policy = policy_change['old_policy'] new_policy = policy_change['new_policy'] policy = policy_change['policy'] if (old_policy and new_policy and old_policy.get('device_only', False) != new_policy.get('device_only', False)): results.append(output_api.PresubmitError( f'In policy {policy}: Released policy device_only status changed.')) # Check device policies have a proto mapping for policy in policy_definitions: if not policy.get('device_only', False): continue policy_name = policy['name'] if (policy_name not in proto_map and policy_name not in legacy_proto_map): results.append(output_api.PresubmitError( f"Please add '{policy_name}' to device_policy_proto_map.yaml and map " "it to the corresponding field in chrome_device_policy.proto.")) # Check that the proto field is equal to the policy name for new policies for policy_change in policy_changelist: if ('old_policy' in policy_change and policy_change['old_policy'] is not None): # Ignore existing policies continue policy_name = policy_change['policy'] field_name = policy_name + ".value" if proto_map[policy_name] != field_name: results.append(output_api.PresubmitError( f"The proto field in chrome_device_policy.proto for '{policy_name}' " "must equal the policy name itself.")) # Check external data max size total_device_policy_external_data_max_size = 0 for policy in policy_definitions: policy_name = policy['name'] if (policy.get('device_only', False) and policy['type'] == 'external'): total_device_policy_external_data_max_size += policy['max_size'] if (total_device_policy_external_data_max_size > TOTAL_DEVICE_POLICY_EXTERNAL_DATA_MAX_SIZE): results.append(output_api.PresubmitError( 'Total sum of device policy external data maximum size limits should not ' f'exceed {TOTAL_DEVICE_POLICY_EXTERNAL_DATA_MAX_SIZE} bytes, current sum ' f'is {total_device_policy_external_data_max_size} bytes.')) return results