[SCM] Samba Shared Repository - branch master updated

Jeremy Allison jra at samba.org
Thu Jul 15 20:04:01 UTC 2021


The branch, master has been updated
       via  f813f8a54ae Update WHATSNEW for Certificate Auto Enrollment
       via  fd6df5356b7 gpo: Test Certificate Auto Enrollment Policy
       via  9f0e6f3c063 gpo: Fix up rsop output of ca certificate
       via  9c0a174af20 gpo: Add Certificate Auto Enrollment Policy
      from  cca9ce5977c WHATSNEW: Start release notes for Samba 4.16.0pre1.

https://git.samba.org/?p=samba.git;a=shortlog;h=master


- Log -----------------------------------------------------------------
commit f813f8a54ae79dd74a99593aeacb252061688807
Author: David Mulder <dmulder at suse.com>
Date:   Mon Jul 12 15:18:04 2021 -0600

    Update WHATSNEW for Certificate Auto Enrollment
    
    Signed-off-by: David Mulder <dmulder at suse.com>
    Reviewed-by: Jeremy Allison <jra at samba.org>
    
    Autobuild-User(master): Jeremy Allison <jra at samba.org>
    Autobuild-Date(master): Thu Jul 15 20:03:45 UTC 2021 on sn-devel-184

commit fd6df5356b7aa180d538a734799b640c1430eb47
Author: David Mulder <dmulder at samba.org>
Date:   Fri Jul 2 20:44:43 2021 +0000

    gpo: Test Certificate Auto Enrollment Policy
    
    Signed-off-by: David Mulder <dmulder at samba.org>
    Reviewed-by: Jeremy Allison <jra at samba.org>

commit 9f0e6f3c0631fdd8bd9580db382d00c2ea4f3c57
Author: David Mulder <dmulder at suse.com>
Date:   Mon Jun 28 09:06:09 2021 -0600

    gpo: Fix up rsop output of ca certificate
    
    Signed-off-by: David Mulder <dmulder at suse.com>
    Reviewed-by: Jeremy Allison <jra at samba.org>

commit 9c0a174af2007476cbff859f962a2667bc5004bf
Author: David Mulder <dmulder at suse.com>
Date:   Thu Jun 17 09:13:12 2021 -0600

    gpo: Add Certificate Auto Enrollment Policy
    
    Signed-off-by: David Mulder <dmulder at suse.com>
    Reviewed-by: Jeremy Allison <jra at samba.org>

-----------------------------------------------------------------------

Summary of changes:
 WHATSNEW.txt                            |  13 ++
 python/samba/gp_cert_auto_enroll_ext.py | 244 ++++++++++++++++++++++++++++++++
 python/samba/gpclass.py                 |   6 +-
 python/samba/tests/bin/cepces-submit    |  15 ++
 python/samba/tests/bin/getcert          |  84 +++++++++++
 python/samba/tests/bin/sscep            |  19 +++
 python/samba/tests/gpo.py               | 124 ++++++++++++++++
 python/samba/tests/usage.py             |   1 +
 source4/scripting/bin/samba-gpupdate    |   2 +
 source4/selftest/tests.py               |   4 +-
 10 files changed, 508 insertions(+), 4 deletions(-)
 create mode 100644 python/samba/gp_cert_auto_enroll_ext.py
 create mode 100755 python/samba/tests/bin/cepces-submit
 create mode 100755 python/samba/tests/bin/getcert
 create mode 100755 python/samba/tests/bin/sscep


Changeset truncated at 500 lines:

diff --git a/WHATSNEW.txt b/WHATSNEW.txt
index f3db6341e06..fe9eff8ba59 100644
--- a/WHATSNEW.txt
+++ b/WHATSNEW.txt
@@ -16,6 +16,19 @@ UPGRADING
 NEW FEATURES/CHANGES
 ====================
 
+Certificate Auto Enrollment
+---------------------------
+
+Certificate Auto Enrollment allows devices to enroll for certificates from
+Active Directory Certificate Services. It is enabled by Group Policy.
+To enable Certificate Auto Enrollment, Samba's group policy will need to be
+enabled by setting the smb.conf option `apply group policies` to Yes. Samba
+Certificate Auto Enrollment depends on certmonger, the cepces certmonger
+plugin, and sscep. Samba uses sscep to download the CA root chain, then uses
+certmonger paired with cepces to monitor the host certificate templates.
+Certificates are installed in /var/lib/samba/certs and private keys are
+installed in /var/lib/samba/private/certs.
+
 
 REMOVED FEATURES
 ================
diff --git a/python/samba/gp_cert_auto_enroll_ext.py b/python/samba/gp_cert_auto_enroll_ext.py
new file mode 100644
index 00000000000..556be604621
--- /dev/null
+++ b/python/samba/gp_cert_auto_enroll_ext.py
@@ -0,0 +1,244 @@
+# gp_cert_auto_enroll_ext samba group policy
+# Copyright (C) David Mulder <dmulder at suse.com> 2021
+#
+# 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 os
+from samba.gpclass import gp_pol_ext
+from samba import Ldb
+from ldb import SCOPE_SUBTREE
+from samba.auth import system_session
+from samba.gpclass import get_dc_hostname
+import base64
+from tempfile import NamedTemporaryFile
+from shutil import move, which
+from subprocess import Popen, PIPE
+import re
+from glob import glob
+import json
+
+cert_wrap = b"""
+-----BEGIN CERTIFICATE-----
+%s
+-----END CERTIFICATE-----"""
+global_trust_dir = '/etc/pki/trust/anchors'
+
+def fetch_certification_authorities(ldb):
+    result = []
+    basedn = ldb.get_default_basedn()
+    dn = 'CN=Certification Authorities,CN=Public Key Services,CN=Services,CN=Configuration,%s' % basedn
+    expr = '(objectClass=certificationAuthority)'
+    res = ldb.search(dn, SCOPE_SUBTREE, expr, ['cn'])
+    if len(res) == 0:
+        return result
+    dn = 'CN=Enrollment Services,CN=Public Key Services,CN=Services,CN=Configuration,%s' % basedn
+    attrs = ['cACertificate', 'cn', 'certificateTemplates', 'dNSHostName']
+    for ca in res:
+        expr = '(cn=%s)' % ca['cn']
+        res2 = ldb.search(dn, SCOPE_SUBTREE, expr, attrs)
+        if len(res) != 1:
+            continue
+        templates = {}
+        for template in res2[0]['certificateTemplates']:
+            templates[template] = fetch_template_attrs(ldb, template)
+        res = dict(res2[0])
+        res['certificateTemplates'] = templates
+        result.append(res)
+    return result
+
+def fetch_template_attrs(ldb, name, attrs=['msPKI-Minimal-Key-Size']):
+    basedn = ldb.get_default_basedn()
+    dn = 'CN=Certificate Templates,CN=Public Key Services,CN=Services,CN=Configuration,%s' % basedn
+    expr = '(cn=%s)' % name
+    res = ldb.search(dn, SCOPE_SUBTREE, expr, attrs)
+    if len(res) == 1 and 'msPKI-Minimal-Key-Size' in res[0]:
+        return dict(res[0])
+    else:
+        return {'msPKI-Minimal-Key-Size': ['2048']}
+
+def format_root_cert(cert):
+    cert = base64.b64encode(cert)
+    return cert_wrap % re.sub(b"(.{64})", b"\\1\n", cert, 0, re.DOTALL)
+
+def find_cepces_submit():
+    certmonger_dirs = [os.environ.get("PATH"), '/usr/lib/certmonger',
+                       '/usr/libexec/certmonger']
+    return which('cepces-submit', path=':'.join(certmonger_dirs))
+
+def get_supported_templates(server):
+    cepces_submit = find_cepces_submit()
+    if os.path.exists(cepces_submit):
+        env = os.environ
+        env['CERTMONGER_OPERATION'] = 'GET-SUPPORTED-TEMPLATES'
+        out, _ = Popen([cepces_submit, '--server=%s' % server], env=env,
+                       stdout=PIPE, stderr=PIPE).communicate()
+        return out.strip().split()
+    return []
+
+def cert_enroll(ca, trust_dir, private_dir, logger):
+    # Install the root certificate chain
+    data = {'files': [], 'templates': []}
+    sscep = which('sscep')
+    if sscep is not None:
+        url = 'http://%s/CertSrv/mscep/mscep.dll/pkiclient.exe?' % \
+            ca['dNSHostName'][0]
+        root_cert = os.path.join(trust_dir, '%s.crt' % ca['cn'])
+        ret = Popen([sscep, 'getca', '-F', 'sha1', '-c',
+                     root_cert, '-u', url]).wait()
+        if ret != 0:
+            logger.warn('sscep failed to fetch the root certificate chain.')
+        root_certs = glob('%s*' % root_cert)
+        data['files'].extend(root_certs)
+        for src in root_certs:
+            # Symlink the certs to global trust dir
+            dst = os.path.join(global_trust_dir, os.path.basename(src))
+            try:
+                os.symlink(src, dst)
+                data['files'].append(dst)
+            except PermissionError:
+                logger.warn('Failed to symlink root certificate to the' +
+                            ' admin trust anchors')
+            except FileNotFoundError:
+                logger.warn('Failed to symlink root certificate to the' +
+                            ' admin trust anchors.' +
+                            ' The directory %s was not found' % \
+                                                        global_trust_dir)
+    else:
+        logger.warn('sscep is not installed, which prevents the installation' +
+                    ' of the root certificate chain.')
+    update = which('update-ca-certificates')
+    if update is not None:
+        Popen([update]).wait()
+    # Setup Certificate Auto Enrollment
+    getcert = which('getcert')
+    cepces_submit = find_cepces_submit()
+    if getcert is not None and os.path.exists(cepces_submit):
+        Popen([getcert, 'add-ca', '-c', ca['cn'][0], '-e',
+               '%s --server=%s' % (cepces_submit, ca['dNSHostName'][0])]).wait()
+        supported_templates = get_supported_templates(ca['dNSHostName'][0])
+        for template, attrs in ca['certificateTemplates'].items():
+            if template not in supported_templates:
+                continue
+            nickname = '%s.%s' % (ca['cn'][0], template.decode())
+            keyfile = os.path.join(private_dir, '%s.key' % nickname)
+            certfile = os.path.join(trust_dir, '%s.crt' % nickname)
+            Popen([getcert, 'request', '-c', ca['cn'][0],
+                   '-T', template.decode(),
+                   '-I', nickname, '-k', keyfile, '-f', certfile,
+                   '-g', attrs['msPKI-Minimal-Key-Size'][0]]).wait()
+            data['files'].extend([keyfile, certfile])
+            data['templates'].append(nickname)
+        if update is not None:
+            Popen([update]).wait()
+    else:
+        logger.warn('certmonger and cepces must be installed for ' +
+                    'certificate auto enrollment to work')
+    return json.dumps(data)
+
+class gp_cert_auto_enroll_ext(gp_pol_ext):
+    def __str__(self):
+        return 'Cryptography\AutoEnrollment'
+
+    def process_group_policy(self, deleted_gpo_list, changed_gpo_list,
+                             trust_dir=None, private_dir=None):
+        if trust_dir is None:
+            trust_dir = self.lp.cache_path('certs')
+        if private_dir is None:
+            private_dir = self.lp.private_path('certs')
+        if not os.path.exists(trust_dir):
+            os.mkdir(trust_dir, mode=0o755)
+        if not os.path.exists(private_dir):
+            os.mkdir(private_dir, mode=0o700)
+
+        for guid, settings in deleted_gpo_list:
+            self.gp_db.set_guid(guid)
+            if str(self) in settings:
+                for ca_cn_enc, data in settings[str(self)].items():
+                    ca_cn = base64.b64decode(ca_cn_enc)
+                    data = json.loads(data)
+                    getcert = which('getcert')
+                    if getcert is not None:
+                        Popen([getcert, 'remove-ca', '-c', ca_cn]).wait()
+                        for nickname in data['templates']:
+                            Popen([getcert, 'stop-tracking',
+                                   '-i', nickname]).wait()
+                    for f in data['files']:
+                        if os.path.exists(f):
+                            os.unlink(f)
+                    self.gp_db.delete(str(self), ca_cn_enc)
+            self.gp_db.commit()
+
+        for gpo in changed_gpo_list:
+            if gpo.file_sys_path:
+                section = 'Software\Policies\Microsoft\Cryptography\AutoEnrollment'
+                self.gp_db.set_guid(gpo.name)
+                pol_file = 'MACHINE/Registry.pol'
+                path = os.path.join(gpo.file_sys_path, pol_file)
+                pol_conf = self.parse(path)
+                if not pol_conf:
+                    continue
+                for e in pol_conf.entries:
+                    if e.keyname == section and e.valuename == 'AEPolicy':
+                        # This policy applies as specified in [MS-CAESO] 4.4.5.1
+                        if e.data == 0x8000:
+                            continue # The policy is disabled
+                        enroll = e.data & 0x1 == 1
+                        manage = e.data & 0x2 == 1
+                        retrive_pending = e.data & 0x4 == 1
+                        if enroll:
+                            url = 'ldap://%s' % get_dc_hostname(self.creds,
+                                                                self.lp)
+                            ldb = Ldb(url=url, session_info=system_session(),
+                                      lp=self.lp, credentials=self.creds)
+                            cas = fetch_certification_authorities(ldb)
+                            for ca in cas:
+                                data = cert_enroll(ca, trust_dir,
+                                                   private_dir, self.logger)
+                                self.gp_db.store(str(self),
+                                     base64.b64encode(ca['cn'][0]).decode(),
+                                     data)
+                        self.gp_db.commit()
+
+    def rsop(self, gpo):
+        output = {}
+        pol_file = 'MACHINE/Registry.pol'
+        section = 'Software\Policies\Microsoft\Cryptography\AutoEnrollment'
+        if gpo.file_sys_path:
+            path = os.path.join(gpo.file_sys_path, pol_file)
+            pol_conf = self.parse(path)
+            if not pol_conf:
+                return output
+            for e in pol_conf.entries:
+                if e.keyname == section and e.valuename == 'AEPolicy':
+                    enroll = e.data & 0x1 == 1
+                    if e.data == 0x8000 or not enroll:
+                        continue
+                    output['Auto Enrollment Policy'] = {}
+                    url = 'ldap://%s' % get_dc_hostname(self.creds, self.lp)
+                    ldb = Ldb(url=url, session_info=system_session(),
+                              lp=self.lp, credentials=self.creds)
+                    cas = fetch_certification_authorities(ldb)
+                    for ca in cas:
+                        policy = 'Auto Enrollment Policy'
+                        cn = ca['cn'][0]
+                        output[policy][cn] = {}
+                        output[policy][cn]['CA Certificate'] = \
+                            format_root_cert(ca['cACertificate'][0]).decode()
+                        output[policy][cn]['Auto Enrollment Server'] = \
+                            ca['dNSHostName'][0]
+                        supported_templates = \
+                            get_supported_templates(ca['dNSHostName'][0])
+                        output[policy][cn]['Templates'] = \
+                            [t.decode() for t in supported_templates]
+        return output
diff --git a/python/samba/gpclass.py b/python/samba/gpclass.py
index 7d3841ba8da..6879719847f 100644
--- a/python/samba/gpclass.py
+++ b/python/samba/gpclass.py
@@ -500,10 +500,10 @@ def __rsop_vals(vals, level=4):
     if type(vals) == dict:
         ret = [' '*level + '[ %s ] = %s' % (k, __rsop_vals(v, level+2))
                 for k, v in vals.items()]
-        return '\n'.join(ret)
+        return '\n' + '\n'.join(ret)
     elif type(vals) == list:
         ret = [' '*level + '[ %s ]' % __rsop_vals(v, level+2) for v in vals]
-        return '\n'.join(ret)
+        return '\n' + '\n'.join(ret)
     else:
         return vals
 
@@ -532,7 +532,7 @@ def rsop(lp, creds, logger, store, gp_extensions, target):
             for section, settings in ext.rsop(gpo).items():
                 print('    Policy Type: %s' % section)
                 print('    ' + ('-'*int(term_width/2)))
-                print(__rsop_vals(settings))
+                print(__rsop_vals(settings).lstrip('\n'))
                 print('    ' + ('-'*int(term_width/2)))
             print('  ' + ('-'*int(term_width/2)))
         print('%s\n' % ('='*term_width))
diff --git a/python/samba/tests/bin/cepces-submit b/python/samba/tests/bin/cepces-submit
new file mode 100755
index 00000000000..1f9d57c6bfb
--- /dev/null
+++ b/python/samba/tests/bin/cepces-submit
@@ -0,0 +1,15 @@
+#!/usr/bin/python3
+import optparse
+import os, sys, re
+
+sys.path.insert(0, "bin/python")
+
+if __name__ == "__main__":
+    parser = optparse.OptionParser('cepces-submit [options]')
+    parser.add_option('--server')
+
+    (opts, args) = parser.parse_args()
+    assert opts.server is not None
+    if 'CERTMONGER_OPERATION' in os.environ and \
+       os.environ['CERTMONGER_OPERATION'] == 'GET-SUPPORTED-TEMPLATES':
+        print('Machine') # Report a Machine template
diff --git a/python/samba/tests/bin/getcert b/python/samba/tests/bin/getcert
new file mode 100755
index 00000000000..93895ebe132
--- /dev/null
+++ b/python/samba/tests/bin/getcert
@@ -0,0 +1,84 @@
+#!/usr/bin/python3
+import optparse
+import os, sys, re
+import pickle
+
+sys.path.insert(0, "bin/python")
+
+if __name__ == "__main__":
+    parser = optparse.OptionParser('getcert <cmd> [options]')
+    parser.add_option('-i')
+    parser.add_option('-c')
+    parser.add_option('-T')
+    parser.add_option('-I')
+    parser.add_option('-k')
+    parser.add_option('-f')
+    parser.add_option('-e')
+    parser.add_option('-g')
+
+    (opts, args) = parser.parse_args()
+    assert len(args) == 1
+    assert args[0] in ['add-ca', 'request', 'remove-ca', 'stop-tracking',
+                       'list', 'list-cas']
+
+    # Use a dir we can write to in the testenv
+    if 'LOCAL_PATH' in os.environ:
+        data_dir = os.path.realpath(os.environ.get('LOCAL_PATH'))
+    else:
+        data_dir = os.path.dirname(os.path.realpath(__file__))
+    dump_file = os.path.join(data_dir, 'getcert.dump')
+    if os.path.exists(dump_file):
+        with open(dump_file, 'rb') as r:
+            cas, certs = pickle.load(r)
+    else:
+        cas = {}
+        certs = {}
+    if args[0] == 'add-ca':
+        # Add a fake CA entry
+        assert opts.c not in cas.keys()
+        cas[opts.c] = opts.e
+    elif args[0] == 'remove-ca':
+        # Remove a fake CA entry
+        assert opts.c in cas.keys()
+        del cas[opts.c]
+    elif args[0] == 'list-cas':
+        # List the fake CAs
+        for ca, helper_location in cas.items():
+            print('CA \'%s\':\n\tis-default: no\n\tca-type: EXTERNAL\n' % ca +
+                  '\thelper-location: %s' % helper_location)
+    elif args[0] == 'request':
+        # Add a fake cert request
+        assert opts.c in cas.keys()
+        assert opts.I not in certs.keys()
+        certs[opts.I] = { 'ca': opts.c, 'template': opts.T,
+                          'keyfile': os.path.abspath(opts.k),
+                          'certfile': os.path.abspath(opts.f),
+                          'keysize': opts.g }
+        # Create dummy key and cert (empty files)
+        with open(opts.k, 'w') as w:
+            pass
+        with open(opts.f, 'w') as w:
+            pass
+    elif args[0] == 'stop-tracking':
+        # Remove the fake cert request
+        assert opts.i in certs.keys()
+        del certs[opts.i]
+    elif args[0] == 'list':
+        # List the fake cert requests
+        print('Number of certificates and requests being tracked: %d.' % \
+              len(certs))
+        for rid, data in certs.items():
+            print('Request ID \'%s\':\n\tstatus: MONITORING\n' % rid +
+                  '\tstuck: no\n\tkey pair storage: type=FILE,' +
+                  'location=\'%s\'' % data['keyfile'] + '\n\t' +
+                  'certificate: type=FILE,location=\'%s\'' % data['certfile'] +
+                  '\n\tCA: %s\n\t' % data['ca'] +
+                  'certificate template/profile: %s\n\t' % data['template'] +
+                  'track: yes\n\tauto-renew: yes')
+
+    if len(cas.items()) == 0 and len(certs.items()) == 0:
+        if os.path.exists(dump_file):
+            os.unlink(dump_file)
+    else:
+        with open(dump_file, 'wb') as w:
+            pickle.dump((cas, certs), w)
diff --git a/python/samba/tests/bin/sscep b/python/samba/tests/bin/sscep
new file mode 100755
index 00000000000..d0d88926766
--- /dev/null
+++ b/python/samba/tests/bin/sscep
@@ -0,0 +1,19 @@
+#!/usr/bin/python3
+import optparse
+import os, sys, re
+
+sys.path.insert(0, "bin/python")
+
+if __name__ == "__main__":
+    parser = optparse.OptionParser('sscep <cmd> [options]')
+    parser.add_option('-F')
+    parser.add_option('-c')
+    parser.add_option('-u')
+
+    (opts, args) = parser.parse_args()
+    assert len(args) == 1
+    assert args[0] == 'getca'
+    assert opts.F == 'sha1'
+    # Create dummy root cert (empty file)
+    with open(opts.c, 'w') as w:
+        pass
diff --git a/python/samba/tests/gpo.py b/python/samba/tests/gpo.py
index 4df0c23c456..b5dc09543ad 100644
--- a/python/samba/tests/gpo.py
+++ b/python/samba/tests/gpo.py
@@ -38,6 +38,7 @@ from samba.vgp_motd_ext import vgp_motd_ext
 from samba.vgp_issue_ext import vgp_issue_ext
 from samba.vgp_access_ext import vgp_access_ext
 from samba.gp_gnome_settings_ext import gp_gnome_settings_ext
+from samba.gp_cert_auto_enroll_ext import gp_cert_auto_enroll_ext
 import logging
 from samba.credentials import Credentials
 from samba.gp_msgs_ext import gp_msgs_ext
@@ -51,6 +52,9 @@ import hashlib
 from samba.gp_parse.gp_pol import GPPolParser
 from glob import glob
 from configparser import ConfigParser
+from samba.gpclass import get_dc_hostname
+from samba import Ldb
+from samba.auth import system_session
 
 realm = os.environ.get('REALM')
 policies = realm + '/POLICIES'
@@ -198,6 +202,28 @@ b"""
 </PolFile>
 """
 
+auto_enroll_reg_pol = \
+b"""
+<?xml version="1.0" encoding="utf-8"?>
+<PolFile num_entries="3" signature="PReg" version="1">
+        <Entry type="4" type_name="REG_DWORD">
+                <Key>Software\Policies\Microsoft\Cryptography\AutoEnrollment</Key>
+                <ValueName>AEPolicy</ValueName>
+                <Value>7</Value>
+        </Entry>
+        <Entry type="4" type_name="REG_DWORD">
+                <Key>Software\Policies\Microsoft\Cryptography\AutoEnrollment</Key>
+                <ValueName>OfflineExpirationPercent</ValueName>
+                <Value>10</Value>
+        </Entry>
+        <Entry type="1" type_name="REG_SZ">
+                <Key>Software\Policies\Microsoft\Cryptography\AutoEnrollment</Key>
+                <ValueName>OfflineExpirationStoreNames</ValueName>
+                <Value>MY</Value>
+        </Entry>
+</PolFile>
+"""
+
 def days2rel_nttime(val):
     seconds = 60
     minutes = 60
@@ -1860,3 +1886,101 @@ class GPOTests(tests.TestCase):
 
         # Unstage the Registry.pol file
         unstage_file(reg_pol)
+
+    def test_gp_cert_auto_enroll_ext(self):
+        local_path = self.lp.cache_path('gpo_cache')
+        guid = '{31B2F340-016D-11D2-945F-00C04FB984F9}'
+        reg_pol = os.path.join(local_path, policies, guid,
+                               'MACHINE/REGISTRY.POL')
+        logger = logging.getLogger('gpo_tests')
+        cache_dir = self.lp.get('cache directory')
+        store = GPOStorage(os.path.join(cache_dir, 'gpo.tdb'))


-- 
Samba Shared Repository



More information about the samba-cvs mailing list