| #!/usr/bin/env python3 | 
 | # Copyright 2022 The Chromium Authors | 
 | # Use of this source code is governed by a BSD-style license that can be | 
 | # found in the LICENSE file. | 
 |  | 
 | import argparse | 
 | import glob | 
 | import json | 
 | import copy | 
 | import os | 
 | import sys | 
 |  | 
 | # This script runs in Chromium and ChromiumOS. | 
 | try: | 
 |   # Chromium. | 
 |   _SRC_PATH = os.path.abspath( | 
 |       os.path.join(os.path.dirname(__file__), '..', '..', '..')) | 
 |   sys.path.append(os.path.join(_SRC_PATH, 'third_party')) | 
 |   import pyyaml | 
 | except ImportError: | 
 |   # ChromiumOS. | 
 |   # Some consumers (ChromiumOS ebuilds) of this script have pyyaml already in | 
 |   # their environment and may not have access to the full Chromium repository | 
 |   # nor the exact file structure. Import based on the Chromium third_party may | 
 |   # not always be possible. | 
 |   # Those consumers may install pyyaml in their environment and import it with | 
 |   # the name of the module instead as per the library documentation. | 
 |   # For compatibility reasons, please refer to | 
 |   # https://source.chromium.org/chromium/chromium/src/+/main:third_party/pyyaml/README.chromium | 
 |   # to know which version of pyyaml to use. | 
 |   import yaml as pyyaml | 
 |  | 
 | 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'),sorted(os.listdir(directory))) | 
 |  | 
 | TEMPLATES_PATH =  os.path.join( | 
 |   os.path.dirname(__file__), 'templates') | 
 |  | 
 | DEFAULT_TEMPLATES_GEN_PATH =  os.path.join( | 
 |   os.path.dirname(__file__), 'policy_templates.json') | 
 |  | 
 | POLICY_DEFINITIONS_KEY = 'policy_definitions' | 
 |  | 
 |  | 
 | def _SubstituteSchemaRefNames(node, child_key, common_schema, parent_refs, | 
 |                               refs_seen): | 
 |   '''Converts recursively objects with the key '$ref' into their actual schema. | 
 |   ''' | 
 |  | 
 |   if (not isinstance(node, dict) or not child_key in node | 
 |     or not isinstance(node[child_key], dict)): | 
 |     return | 
 |   if '$ref' in node[child_key]: | 
 |     ref_name = node[child_key]['$ref'] | 
 |     # If the parent has the same id as child, leave the child's ref to avoid | 
 |     # infinite loop. | 
 |     if ref_name not in parent_refs: | 
 |       node[child_key] = copy.deepcopy(common_schema[ref_name]) | 
 |       parent_refs.add(ref_name) | 
 |     # If the schema has been seen already, only keep a reference to the first to | 
 |     # avoid having the same ref defined at multiple places. | 
 |     if ref_name not in refs_seen: | 
 |       node[child_key]['id'] = ref_name | 
 |     refs_seen.add(ref_name) | 
 |  | 
 |   for ck in sorted(node[child_key].keys()): | 
 |     # Copy parents ref so that parents are unique for each child branch and do | 
 |     # not mix with sibling nodes. | 
 |     _SubstituteSchemaRefNames(node[child_key], ck, common_schema, | 
 |                               parent_refs.copy(), refs_seen) | 
 |  | 
 |  | 
 | def _SubstituteSchemaRefs(policies, common_schema): | 
 |   '''Converts objects with the key '$ref' into their actual schema. | 
 |  | 
 |     Args: | 
 |       policies: List of policies. | 
 |       common_schema: Dictionary of schemas by their ref names.''' | 
 |   policy_list = [policy for policy in policies if 'schema' in policy] | 
 |  | 
 |   refs_seen = set() | 
 |   for policy in sorted(policy_list, key=lambda policy: policy['id']): | 
 |     parent_refs = set() | 
 |     _SubstituteSchemaRefNames(policy, 'schema', common_schema, parent_refs, | 
 |                               refs_seen) | 
 |     parent_refs = set() | 
 |     _SubstituteSchemaRefNames(policy, 'validation_schema', common_schema, | 
 |                               parent_refs, refs_seen) | 
 |  | 
 |  | 
 | def _BuildPolicyTemplate(data): | 
 |   ''' | 
 |   Converts data into the format of the old policy_templates.json so that it | 
 |   can be used in policy_templates.grd | 
 |  | 
 |   Schema : { | 
 |     "policy_definitions": { | 
 |       "type": "list", | 
 |       "items": { | 
 |         "id": { "type": "number" }, | 
 |         "name": "string", | 
 |         "policies": { "type": "list", "items": "string" } // for policy groups. | 
 |         // See components/policy/resources/new_policy_templates/policy.yaml | 
 |         // for the other variables/ | 
 |       } | 
 |     }, | 
 |     "messages": { | 
 |       "type": "Object", | 
 |       "patternProperties": { | 
 |         "[A-Za-z]": { "desc": "string", "text": "String" } | 
 |       } | 
 |     }, | 
 |     //components/policy/resources/templates/manual_device_policy_proto_map.yaml | 
 |     // Includes policies where generate_device_proto is true. | 
 |     "device_policy_proto_map": { | 
 |       "type": "Object", | 
 |       "patternProperties": { | 
 |         "[A-Za-z]": { "type": "Object", "properties": "String" } | 
 |       } | 
 |     }, | 
 |     // Only lists of 2 items | 
 |     "legacy_device_policy_proto_map": { "type": "list", "items": "String" }, | 
 |     //components/policy/resources/templates/messages.yaml | 
 |     "messages": { | 
 |       "type": "Object", | 
 |       "patternProperties": { | 
 |         "[A-Za-z]": { | 
 |           "type": "Object", | 
 |           "properties": { "text": string, "description": "string" } | 
 |       } | 
 |     }, | 
 |     "risk_tag_definitions": { | 
 |       "type": "list", | 
 |       "items": { | 
 |         "name": "string", | 
 |         "description": "string", | 
 |         "user-description": "string" | 
 |       } | 
 |     }, | 
 |     "policy_atomic_group_definitions": { | 
 |       "type": "list", | 
 |       "items": { | 
 |         "id": { "type": "number" }, | 
 |         "name": "string", | 
 |         "caption": "string", | 
 |         "policies": { "type": "list", "items": "string" } // for policy groups. | 
 |         // See components/policy/resources/new_policy_templates/policy.yaml | 
 |         // for the other variables/ | 
 |       } | 
 |     }, | 
 |     "placeholders": { "type": "list" }, | 
 |     "deleted_policy_ids": { "type": "list", "items": { "type": "number" } }, | 
 |     "deleted_atomic_policy_group_ids": { | 
 |       "type": "list", | 
 |       "items": { "type": "number" } | 
 |     }, | 
 |     "highest_id_currently_used": {  "type": "number" }, | 
 |     "highest_atomic_group_id_currently_used": {  "type": "number" }, | 
 |   } | 
 |   ''' | 
 |  | 
 |   policy_name_id = { name: id for id, name | 
 |     in data['policies']['policies'].items() if name } | 
 |   atomic_group_name_id = { name: id | 
 |     for id, name in data['policies']['atomic_groups'].items() if name } | 
 |  | 
 |   policy_groups = [{ | 
 |         'name': group_name, | 
 |         'type': 'group', | 
 |         'caption': group['caption'], | 
 |         'desc': group['desc'], | 
 |         'policies': list(group['policies']) | 
 |     } for group_name, group in data[POLICY_DEFINITIONS_KEY].items()] | 
 |  | 
 |   policies = [] | 
 |   atomic_groups = [] | 
 |   for group in data[POLICY_DEFINITIONS_KEY].values(): | 
 |     for policy_name, policy in group['policies'].items(): | 
 |       policies.append({ | 
 |         'id': policy_name_id[policy_name], | 
 |         'name': policy_name, **policy | 
 |       }) | 
 |  | 
 |     for name, atomic_group in group['policy_atomic_groups'].items(): | 
 |       atomic_groups.append({ | 
 |         'id': atomic_group_name_id[name], | 
 |         'name': name, **atomic_group | 
 |       }) | 
 |  | 
 |   device_policy_proto_map = data['manual_device_policy_proto_map'].copy() | 
 |  | 
 |   for policy in policies: | 
 |     if not policy.get('device_only', False): | 
 |       continue | 
 |  | 
 |     if not policy.get('generate_device_proto', True): | 
 |       continue | 
 |  | 
 |     device_policy_proto_map[policy['name']] = policy['name'] + '.value' | 
 |  | 
 |   result = { | 
 |       POLICY_DEFINITIONS_KEY: policies + policy_groups, | 
 |       'deleted_policy_ids': | 
 |       [id for id, name in data['policies']['policies'].items() if not name], | 
 |       'highest_id_currently_used': len(data['policies']['policies']), | 
 |       'policy_atomic_group_definitions': atomic_groups, | 
 |       'deleted_atomic_policy_group_ids': [ | 
 |           id for id, name in data['policies']['atomic_groups'].items() | 
 |           if not name | 
 |       ], | 
 |       'highest_atomic_group_id_currently_used': | 
 |       len(data['policies']['atomic_groups']), | 
 |       'placeholders': [], | 
 |       'legacy_device_policy_proto_map': [], | 
 |       'device_policy_proto_map': device_policy_proto_map, | 
 |       'messages': data['messages'], | 
 |       'risk_tag_definitions': [{'name': name, **value} | 
 |         for name, value in data['risk_tag_definitions'].items()] | 
 |   } | 
 |   for key, values in data['legacy_device_policy_proto_map'].items(): | 
 |     for item in values: | 
 |       result['legacy_device_policy_proto_map'].append([key, item]) | 
 |  | 
 |   _SubstituteSchemaRefs(result[POLICY_DEFINITIONS_KEY], data['common_schemas']) | 
 |  | 
 |   return result | 
 |  | 
 |  | 
 | def _GetMetadata(): | 
 |   '''Returns an object containing the policy metadata in order to build the | 
 |      policy definition template.''' | 
 |   result = {} | 
 |   for file in _SafeListDir(TEMPLATES_PATH): | 
 |     filename = os.fsdecode(file) | 
 |     file_basename, file_extension = os.path.splitext(filename) | 
 |     if not file_extension == ".yaml": | 
 |       continue | 
 |     with open(os.path.join(TEMPLATES_PATH, filename), encoding='utf-8') as f: | 
 |       result[file_basename] = pyyaml.safe_load(f) | 
 |   return result | 
 |  | 
 |  | 
 | def _GetPoliciesAndGroups(): | 
 |   '''Returns an object containing the policy groups with their details, policies | 
 |      and atomic policy groups in order to build the policy definition template. | 
 |   ''' | 
 |   result = {} | 
 |   policy_definitions_path = os.path.join(TEMPLATES_PATH, POLICY_DEFINITIONS_KEY) | 
 |   for group_name in _SafeListDir(policy_definitions_path): | 
 |     result[group_name] = {'policies': {}, 'policy_atomic_groups': {}} | 
 |     group_path = os.path.join(policy_definitions_path, group_name) | 
 |     if not os.path.isdir(group_path): | 
 |       continue | 
 |  | 
 |     for file in _SafeListDir(group_path): | 
 |       filename = os.fsdecode(file) | 
 |       file_basename, file_extension = os.path.splitext(filename) | 
 |       file_path = os.path.join(group_path, filename) | 
 |  | 
 |       if file_extension != '.yaml': | 
 |         continue | 
 |  | 
 |       with open(file_path, encoding='utf-8') as f: | 
 |         data = pyyaml.safe_load(f) | 
 |       if file_basename == '.group.details': | 
 |         result[group_name].update(data) | 
 |       elif file_basename == 'policy_atomic_groups': | 
 |         result[group_name]['policy_atomic_groups'].update(data) | 
 |       else: | 
 |         result[group_name]['policies'][file_basename] = data | 
 |   return result | 
 |  | 
 | def _LoadPolicies(): | 
 |   ''' | 
 |   Loads all the yaml files used to define policies and their metadata into a | 
 |   single object. This is a direct representation of the data structure of the | 
 |   directories and their files. | 
 |  | 
 |   Schema : { | 
 |     //components/policy/resources/templates/policies.yaml | 
 |     "policies":{ | 
 |       // Map of policy atomic group ID to policy atomic group name. | 
 |       "atomic_groups": { | 
 |         "type": "Object", | 
 |         "patternProperties": { | 
 |           "[A-Za-z]": { "type": "Object", "properties": "String" } | 
 |         } | 
 |       } | 
 |       // Map of policy ID to policy name. | 
 |       "policies": { | 
 |         "type": "Object", | 
 |         "patternProperties": { | 
 |           "[A-Za-z]": { "type": "Object", "properties": "String" } | 
 |         } | 
 |       } | 
 |     } | 
 |     "policy_definitions": { | 
 |         "type": "Object", | 
 |         "patternProperties": { | 
 |           // Dictionary of policy groups | 
 |           "[A-Za-z]": { | 
 |             "type": "Object", | 
 |             // c/policy/resources/new_policy_templates/.groups.details.yaml | 
 |             "properties": { | 
 |               "caption": {"type": "string"}, | 
 |               "description": {"type": "string"} | 
 |             }, | 
 |             "patternProperties": { | 
 |               // Dictionary of policies | 
 |               "[A-Za-z]": { | 
 |                 "type": "Object", | 
 |                 "properties": { | 
 |                   // c/policy/resources/new_policy_templates/policy.yaml | 
 |                 } | 
 |               } | 
 |             } | 
 |           } | 
 |         } | 
 |       } | 
 |     }, | 
 |     // components/policy/resources/templates/manual_device_policy_proto_map.yaml | 
 |     "manual_device_policy_proto_map": { | 
 |       "type": "Object", | 
 |       "patternProperties": { | 
 |         "[A-Za-z]": { "type": "Object", "properties": "String" } | 
 |       } | 
 |     }, | 
 |     // components/policy/resources/templates/legacy_device_policy_proto_map.yaml | 
 |     "legacy_device_policy_proto_map": { | 
 |       "type": "Object", | 
 |       "patternProperties": { | 
 |         "[A-Za-z]": { "type": "list", "items": "String" } | 
 |       } | 
 |     }, | 
 |     // components/policy/resources/templates/messages.yaml | 
 |     "messages": { | 
 |       "type": "Object", | 
 |       "patternProperties": { | 
 |         "[A-Za-z]": { | 
 |           "type": "Object", | 
 |           "properties": { "text": string, "description": "string" } | 
 |         } | 
 |       } | 
 |     }, | 
 |     // components/policy/resources/templates/risk_tag_definitions.yaml | 
 |     "risk_tag_definitions": { | 
 |       "type": "Object", | 
 |       "patternProperties": { | 
 |         "[A-Za-z]": { | 
 |           "type": "Object", | 
 |           "properties": { "description": string, "user-description": "string" } | 
 |         } | 
 |       } | 
 |     } | 
 |   } | 
 |   ''' | 
 |   return { | 
 |     POLICY_DEFINITIONS_KEY: _GetPoliciesAndGroups(), | 
 |     **_GetMetadata() | 
 |   } | 
 |  | 
 | def GetPolicyTemplates(): | 
 |   '''Returns an object containing the policy templates. | 
 |   ''' | 
 |   template = _LoadPolicies() | 
 |   return _BuildPolicyTemplate(template) | 
 |  | 
 |  | 
 | def _WriteDepFile(dep_file, target, source_files): | 
 |   '''Writes a dep file for `target` at `dep_file` with `source_files` as the | 
 |      dependencies. | 
 |  | 
 |     Args: | 
 |       dep_file: A path to the dependencies file for this script. | 
 |       target: The build target. | 
 |       source_files: A list of the dependencies for the build target. | 
 |   ''' | 
 |   with open(dep_file, "w") as f: | 
 |     f.write(target) | 
 |     f.write(": ") | 
 |     f.write(' '.join(source_files)) | 
 |  | 
 |  | 
 | def main(): | 
 |   '''Generates the a JSON file at `dest` with all the policy definitions. | 
 |      If `dest` is not specified, a file name 'policy_templates.json' will be | 
 |      generated in the same directory as the script. | 
 |  | 
 |     Args: | 
 |       dest: A path to the policy templates generated definitions. | 
 |       depfile: A path to the dependencies file for this script. | 
 |   ''' | 
 |   parser = argparse.ArgumentParser() | 
 |   parser.add_argument('--dest', dest='dest') | 
 |   parser.add_argument('--depfile', dest='deps_file') | 
 |  | 
 |   args = parser.parse_args() | 
 |   if args.dest: | 
 |     path = os.path.join(args.dest) | 
 |   else: | 
 |     path = DEFAULT_TEMPLATES_GEN_PATH | 
 |  | 
 |   policy_templates = GetPolicyTemplates() | 
 |   with open(path, 'w+', encoding='utf-8') as dest: | 
 |     json.dump(policy_templates, dest, indent=2, sort_keys=True) | 
 |  | 
 |   files = sorted([f.replace('\\', '/') | 
 |     for f in glob.glob(TEMPLATES_PATH + '/**/*.yaml', recursive=True)]) | 
 |  | 
 |   if args.deps_file: | 
 |     _WriteDepFile(args.deps_file, args.dest, files) | 
 |  | 
 | if '__main__' == __name__: | 
 |   sys.exit(main()) |