#!/usr/bin/python3                                                                                                         
  
# Script for show ALT Domain Policies templates
# Copyright (C) 2019 Andrey Cherepanov <cas@altlinux.org>

# This program is free software: you can redistribute it and/or modify it under
# the terms of the GNU General Public License as published by the Free Software
# Foundation, either version 3 of the License, or (at your option) any later
# version.
#
# This program 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 General Public License for more
# details.
#
# You should have received a copy of the GNU General Public License along with
# this program. If not, see http://www.gnu.org/licenses/.

import sys
import os
import glob
from collections import OrderedDict
import xml.etree.ElementTree as ET
from xml.dom import minidom
import json
import base64
import urllib.parse
import subprocess
import re
import pprint

TEMPLATE_PATH = "/usr/libexec/adp"
DEBUG = False

# Required mapping
nsmap = {"xml": "http://www.w3.org/XML/1998/namespace"}

if len( sys.argv ) < 4:
    print( "Usage: adp-helper <cmd> <policy> <class>" )
    sys.exit( 1 )

cmd = sys.argv[1]
policy = sys.argv[2]
item_class = sys.argv[3]
f_debug = None

# Process passed language
lang = os.environ['in_language'] if 'in_language' in os.environ else ''
lang = lang.split( ';' )[0]   # Preferred locale as ru_RU
lang = lang.split( '_' )[0]   # Language without region as 'ru'

def get_list():
    """Get templates list"""
    files = [ os.path.basename( os.path.splitext( f )[0] ) for f in glob.glob( TEMPLATE_PATH + "/*.xml", recursive=False ) ]
    return files

def adp_get_policy_path():
    """Get policy path"""
    out = subprocess.check_output( "samba-tool domain info 127.0.0.1".split( ' ' ), stderr=subprocess.STDOUT ).decode()
    d = re.search( "^Domain\s*: (\S+)\n", out, re.MULTILINE )
    if d:
        return "/var/lib/samba/sysvol/%s/Policies" % ( d.group( 1 ) )
    return ""

def adp_get_rule_file( policy, classname ):
    """Get rule file if it exists"""
    # Check policy directory
    top_dir = os.path.join( adp_get_policy_path(), policy )
    loc = os.path.join( top_dir, classname.capitalize() )
    if not os.path.isdir( loc ):
        # Possible, last part in path has wrong case
        loc = os.path.join( top_dir, classname.upper() )
        if not os.path.isdir( loc ):
            loc = os.path.join( top_dir, classname.lower() )
            if not os.path.isdir( loc ):
                return ""

    # Check if Linux.xml exists in policy directory
    file_name = os.path.join( loc, 'Linux.xml' )
    return file_name

def adp_rule_list( file_name ):
    """Return dictionary of templates with filled parameters"""
    d = {}

    if not file_name:
        return d

    try:
        t = ET.parse( file_name )
    except:
        return d

    for i in t.findall( 'policy' ):
        # Iterate through each filled policy
        key = i.findtext( 'template' )
        values = {}
        for v in i.findall( 'param' ):
            name = v.attrib.get( 'name' )
            value = v.text
            if name:
                values[ name ] = value
        d[ key ] = values
    return d

def adp_list( policy, item_class, is_print ):
    """Return list of available rules for policy and class with param values"""

    # Get templates list
    l = get_list()
    l.sort()

    # Get filled Linux.xml file content
    dp = adp_rule_list( adp_get_rule_file( policy, item_class ) )

    d = list()
    for i in l:
        # Iterate each template
        try:
            t = ET.parse( os.path.join( TEMPLATE_PATH, i + ".xml" ) )
        except:
            continue

        # Check class
        appropriated_class = False
        for k in t.findall( "class" ):
            # Check both for machine and user
            if item_class == k.text:
                appropriated_class = True
            if item_class == 'machine' and k.text == 'su':
                appropriated_class = True

        if not appropriated_class:
            continue

        id = t.findtext( "id" )
        tag = 'name'
        summary_en = t.findtext( tag, namespaces=nsmap )
        if lang:
            tag = "%s[@xml:lang=\"%s\"]" % ( tag, lang )
        summary = t.findtext( tag, namespaces=nsmap )

        # Fallback to English if localized item is empty
        if not summary:
            summary = summary_en

        params = t.findall( "parameters/param", namespaces=nsmap  )

        # Create new item
        item = { 'id': id, 'summary': summary, 'is_active': id in dp, 'params': list() }

        # Add parameters
        for p in params:

            # Fill name, type and default
            if 'name' in p.attrib:
                name = p.attrib['name']
            else:
                name = p.text
            if 'type' in p.attrib:
                type = p.attrib['type']
            else:
                type = p.text
            default = p.attrib['default'] if 'default' in p.attrib else ''

            # Localized label
            label = p.findtext( "label[@xml:lang=\"%s\"]" % ( lang ), namespaces=nsmap )
            if not label:
                label = name

            # Add item
            v = ''

            # Check for specified value
            if id in dp and name in dp[ id ]:
                v = dp[ id ][ name ]

            item['params'].append( { 'name': name, 'type': type, 'label': label, 'default': default, 'value': v } )

        # Add item to policy list
        d.append( item )

        # Serialize params
        params = json.dumps( item['params'], indent=2 )
        params = base64.standard_b64encode( params.encode() ).decode()

        if is_print:
            print( "%s|%s|%s" % ( id, summary, params ) )
    return d

def adp_write( policy, item_class ):
    """Write parameters for all rules for policy and class (encoded in base64)"""
    global f_debug

    if DEBUG:
        f_debug = open( "/tmp/adp-helper.log", "a" )
        f_debug.write( "WRITE %s (%s)\n" % ( policy, item_class ) )
    data = "\n".join( sys.stdin.readlines() ).strip()
    if DEBUG:
        f_debug.write( "%s\n" % ( data ) )
    # Remove double quotes
    #if len(data) > 1 and data[0] == '"':
    #    data = data[1:-1]
    #if DEBUG:
    #    f_debug.write( "%s\n" % ( data ) )
    enabled_items = []
    if data:
        # Decode from base64 and parse complete URL
        #data = base64.b64decode( data ).decode()
        # Decode urlencoded data
        data = urllib.parse.parse_qs( data )

        if DEBUG:
            f_debug.write( "Decoding is complete\n" )
            f_debug.write( "%s\n" % ( pprint.pformat( data ) ) )

        # Extract parameter values
        ddata = {}
        for key, val in data.items():
            value = val[0] if len( val ) > 0 else ''
            d = list( filter( None, re.split( '\[|\]', key ) ) )
            # Only for rules values
            if len(d) > 2 and d[0] == 'rules':
                id, template, param = d

                # Oops, template has all dots replaced by _
                template = template.replace( '_', '.' )

                if DEBUG:
                    f_debug.write( "%s:%s = %s\n" % ( template, param, value ) )

                # If enabled == on add to enabled
                if param == 'enabled':
                    if value == 'on' or value == 'true':
                        enabled_items.append( template )
                    continue

                # Add new rule
                if not template in ddata:
                    ddata[ template ] = {}

                # Add parameter value
                ddata[ template ][ param ] = value

        if DEBUG:
            f_debug.write( "Enabled items: %s\n" % ( pprint.pformat( enabled_items ) ) )
        # Dictionary cleanup for disabled items
        dd = {}
        for di in enabled_items:
            dd[ di ] = ddata[ di ] if di in ddata else {}

        # Dump clean dictionary with values
        if DEBUG:
            f_debug.write( "Data: %s\n" % ( pprint.pformat( dd ) ) )

        # Write data
        adp_write_args( policy, item_class, dd )
    else:
        adp_write_args( policy, item_class, {} )
    if DEBUG:
        f_debug.close()
    return 0

def adp_write_args( policy, item_class, data ):
    """Write parameters for all rules for policy and class"""
    global f_debug

    file_name = adp_get_rule_file( policy, item_class )
    if not file_name:
        return 1

    # Create empty file
    t = ET.ElementTree( ET.Element( 'policies' ) )

    # Sort by keys
    data = OrderedDict( sorted( data.items() ) )

    for id in data:
        i = ET.Element( 'policy' )
        template = ET.Element( 'template' )
        template.text = id
        i.append( template )
        # Iterate through all parameters
        for param, value in data[ id ].items():
            p = ET.Element( 'param', { 'name': param } )
            p.text = value
            i.append( p )
        # Save rule block
        t.getroot().append( i )

    # Dumps result
    xmlstr = minidom.parseString( ET.tostring( t.getroot() ) ).toprettyxml( indent="    ", encoding="UTF-8" ).decode()
    if DEBUG:
        f_debug.write( "Writing file %s\n" % ( file_name ) )
        f_debug.write( xmlstr )

    # Write result
    with open( file_name, "w" ) as f_policy:
        f_policy.write( xmlstr )

    return 0

if cmd == 'list':
    adp_list( policy, item_class, True )
elif cmd == 'json':
    d = adp_list( policy, item_class, False )
    params = json.dumps( d )
    params = urllib.parse.quote( params )
    print( params )
elif cmd == 'json_debug':
    d = adp_list( policy, item_class, False )
    params = json.dumps( d, indent = 2 )
    print( params )
elif cmd == 'write':
    adp_write( policy, item_class )
elif cmd == 'write_args':
    data = { 'user.background.image': { 'location':'smb://dc0.alt.domain/netlogon/TS/urist.jpg' }, 'user.screensaver.lock': { 'state': 'true' } }
    ret = adp_write_args( policy, item_class, data )
