From 61d11001dd583e8a05107206ccd424b53c63291a Mon Sep 17 00:00:00 2001 From: Gabriel Nagy Date: Wed, 26 Jul 2023 02:05:00 +0300 Subject: [PATCH 01/13] Hack to allow propagation of Microsoft GPO entries The certificate policy manager is configured via the Windows GPO entries instead of our custom ADMX/ADML counterpart. We were already able to parse these entries but we were excluding them as they didn't start with our Ubuntu-specific prefix. The easiest way to make this work is to add the Ubuntu prefix to the keys that we want to use in our certificate policy manager. --- internal/ad/ad.go | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/internal/ad/ad.go b/internal/ad/ad.go index 9f55fa18b..e8b21e9bc 100644 --- a/internal/ad/ad.go +++ b/internal/ad/ad.go @@ -41,6 +41,13 @@ const ( UserObject ObjectClass = "user" // ComputerObject is a computer representation in AD. ComputerObject ObjectClass = "computer" + + // certAutoEnrollKey is the GPO entry that configures certificate autoenrollment. + certAutoEnrollKey string = "Software/Policies/Microsoft/Cryptography/AutoEnrollment/AEPolicy" + + // policyServerPrefix is the GPO prefix containing keys that configure + // policy servers for certificate enrollment. + policyServersPrefix string = "Software/Policies/Microsoft/Cryptography/PolicyServers/" ) type gpo downloadable @@ -522,6 +529,16 @@ func (ad *AD) parseGPOs(ctx context.Context, gpos []gpo, objectClass ObjectClass var currentKey string var overrideEnabled bool for _, pol := range pols { + // Rewrite the certificate autoenrollment key so we can easily + // use it in the policy manager + if pol.Key == certAutoEnrollKey { + pol.Key = fmt.Sprintf("%scertificate/autoenroll/all", keyFilterPrefix) + } + + if strings.HasPrefix(pol.Key, policyServersPrefix) { + pol.Key = fmt.Sprintf("%scertificate/%s/all", keyFilterPrefix, pol.Key) + } + // Only consider supported policies for this distro if !strings.HasPrefix(pol.Key, keyFilterPrefix) { continue From c5f50a64454574c2229589643ab4b95f89f47377 Mon Sep 17 00:00:00 2001 From: Gabriel Nagy Date: Wed, 26 Jul 2023 02:09:39 +0300 Subject: [PATCH 02/13] Pass backend object to policies.NewManager We will need to know both the domain and online status of the backend for the certificate policy manager. Since the backend object contains both of these, pass it along when creating the policy manager object. --- internal/adsysservice/adsysservice.go | 2 +- internal/policies/manager.go | 6 +++++- internal/policies/manager_test.go | 24 +++++++++++++++++++++--- 3 files changed, 27 insertions(+), 5 deletions(-) diff --git a/internal/adsysservice/adsysservice.go b/internal/adsysservice/adsysservice.go index 34f95f6a3..76926ac4c 100644 --- a/internal/adsysservice/adsysservice.go +++ b/internal/adsysservice/adsysservice.go @@ -282,7 +282,7 @@ func New(ctx context.Context, opts ...option) (s *Service, err error) { if args.systemUnitDir != "" { policyOptions = append(policyOptions, policies.WithSystemUnitDir(args.systemUnitDir)) } - m, err := policies.NewManager(bus, hostname, policyOptions...) + m, err := policies.NewManager(bus, hostname, adBackend, policyOptions...) if err != nil { return nil, err } diff --git a/internal/policies/manager.go b/internal/policies/manager.go index 0c77642a1..6a94c4a93 100644 --- a/internal/policies/manager.go +++ b/internal/policies/manager.go @@ -33,6 +33,7 @@ import ( "time" "github.com/godbus/dbus/v5" + "github.com/ubuntu/adsys/internal/ad/backends" "github.com/ubuntu/adsys/internal/consts" log "github.com/ubuntu/adsys/internal/grpc/logstreamer" "github.com/ubuntu/adsys/internal/i18n" @@ -66,6 +67,8 @@ type Manager struct { gdm *gdm.Manager apparmor *apparmor.Manager proxy *proxy.Manager + backend backends.Backend + subscriptionDbus dbus.BusObject @@ -195,7 +198,7 @@ func WithSystemdCaller(p systemdCaller) Option { } // NewManager returns a new manager with all default policy handlers. -func NewManager(bus *dbus.Conn, hostname string, opts ...Option) (m *Manager, err error) { +func NewManager(bus *dbus.Conn, hostname string, backend backends.Backend, opts ...Option) (m *Manager, err error) { defer decorate.OnError(&err, i18n.G("can't create a new policy handlers manager")) defaultSystemdCaller, err := systemd.New(bus) @@ -272,6 +275,7 @@ func NewManager(bus *dbus.Conn, hostname string, opts ...Option) (m *Manager, er dbus.ObjectPath(consts.SubscriptionDbusObjectPath)) return &Manager{ + backend: backend, policiesCacheDir: policiesCacheDir, hostname: hostname, dconf: dconfManager, diff --git a/internal/policies/manager_test.go b/internal/policies/manager_test.go index 018a68dc9..67f70c3af 100644 --- a/internal/policies/manager_test.go +++ b/internal/policies/manager_test.go @@ -98,6 +98,7 @@ func TestApplyPolicies(t *testing.T) { m, err := policies.NewManager(bus, hostname, + mockBackend{}, policies.WithCacheDir(cacheDir), policies.WithRunDir(runDir), policies.WithDconfDir(dconfDir), @@ -282,7 +283,7 @@ func TestDumpPolicies(t *testing.T) { t.Parallel() cacheDir, runDir := t.TempDir(), t.TempDir() - m, err := policies.NewManager(bus, hostname, policies.WithCacheDir(cacheDir), policies.WithRunDir(runDir)) + m, err := policies.NewManager(bus, hostname, mockBackend{}, policies.WithCacheDir(cacheDir), policies.WithRunDir(runDir)) require.NoError(t, err, "Setup: couldn’t get a new policy manager") err = os.MkdirAll(filepath.Join(cacheDir, policies.PoliciesCacheBaseName), 0750) @@ -349,7 +350,7 @@ func TestLastUpdateFor(t *testing.T) { t.Parallel() cacheDir, runDir := t.TempDir(), t.TempDir() - m, err := policies.NewManager(bus, hostname, policies.WithCacheDir(cacheDir), policies.WithRunDir(runDir)) + m, err := policies.NewManager(bus, hostname, mockBackend{}, policies.WithCacheDir(cacheDir), policies.WithRunDir(runDir)) require.NoError(t, err, "Setup: couldn’t get a new policy manager") err = os.MkdirAll(filepath.Join(cacheDir, policies.PoliciesCacheBaseName), 0750) @@ -411,7 +412,7 @@ func TestGetSubscriptionState(t *testing.T) { }() cacheDir, runDir := t.TempDir(), t.TempDir() - m, err := policies.NewManager(bus, hostname, policies.WithCacheDir(cacheDir), policies.WithRunDir(runDir)) + m, err := policies.NewManager(bus, hostname, mockBackend{}, policies.WithCacheDir(cacheDir), policies.WithRunDir(runDir)) require.NoError(t, err, "Setup: couldn’t get a new policy manager") got := m.GetSubscriptionState(context.Background()) @@ -435,3 +436,20 @@ func (d *mockProxyApplier) Call(_ string, _ dbus.Flags, _ ...interface{}) *dbus. return &dbus.Call{Err: errApply} } + +// mockBackend is a mock for the backend object. +type mockBackend struct { + wantOnlineErr bool +} + +func (m mockBackend) Domain() string { return "example.com" } +func (m mockBackend) ServerURL(context.Context) (string, error) { return "adc.example.com", nil } +func (m mockBackend) HostKrb5CCName() (string, error) { return "/tmp/krb5cc_0", nil } +func (m mockBackend) DefaultDomainSuffix() string { return "example.com" } +func (m mockBackend) IsOnline() (bool, error) { + if m.wantOnlineErr { + return false, errors.New("mock error") + } + return true, nil +} +func (m mockBackend) Config() string { return "mock config" } From 0f4d632f8788fd2e3d06a2ebf625d577b47b82fb Mon Sep 17 00:00:00 2001 From: Gabriel Nagy Date: Wed, 26 Jul 2023 02:14:44 +0300 Subject: [PATCH 03/13] Add certificate policy manager This manager leverages the Samba implementation of certificate autoenrollment. It contains a few patches and fixes on top of the samba#master version of `gp_cert_auto_enroll_ext.py` file, that will ultimately be upstreamed. Autoenrollment is performed via a separate policy manager that runs a helper Python script (`cert-autoenroll`) which communicates with the Windows CEP/CES services through Samba. For better control and to avoid unexpected behavior we vendor the required Samba files, which are confirmed to work on all Ubuntu versions starting with (and including) Jammy (22.04). Samba has its own cache mechanism which stores information concerning the applied GPOs which we are using in order to ensure idempotency. By default, Samba would parse the .reg file itself (see the process_group_policy method from the vendored code). However, it is better to have this functionality entirely within adsys so we can provide samba the pre-parsed list of GPO entries and override the entry point of the extension. This ensures we don't operate on disk files which can change at anytime (even during adsys policy application). Doing this we also have better knowledge on the enabled/disabled state of the GPO entry used to configure the policy. The advanced configuration entries are passed via JSON to the external script which then takes care to create the proper PReg entries that can be used by Samba to apply additional logic when determining the policy servers to use. Fixes UDENG-1056 --- internal/consts/consts.go | 6 + internal/policies/certificate/cert-autoenroll | 131 +++ .../certificate/cert-autoenroll_test.go | 142 +++ internal/policies/certificate/certificate.go | 259 +++++ .../policies/certificate/certificate_test.go | 154 +++ .../gp/gp_cert_auto_enroll_ext.py | 536 +++++++++++ .../python/vendor_samba/gp/gpclass.py | 884 ++++++++++++++++++ .../python/vendor_samba/gp/util/logging.py | 112 +++ .../enroll_with_empty_advanced_configuration | 11 + .../golden/enroll_with_simple_configuration | 11 + ...ith_simple_configuration_and_debug_enabled | 12 + .../enroll_with_valid_advanced_configuration | 48 + .../TestCertAutoenrollScript/golden/unenroll | 8 + .../golden/computer,_configured_to_enroll | 3 + ...nfigured_to_enroll,_advanced_configuration | 3 + .../golden/computer,_configured_to_unenroll | 3 + .../admock/samba/credentials/__init__.py | 3 + .../testutils/admock/samba/dcerpc/preg.py | 9 + .../testutils/admock/samba/param/__init__.py | 8 +- .../testutils/admock/vendor_samba/__init__.py | 0 .../admock/vendor_samba/gp/__init__.py | 0 .../gp/gp_cert_auto_enroll_ext.py | 38 + .../admock/vendor_samba/gp/gpclass.py | 4 + 23 files changed, 2383 insertions(+), 2 deletions(-) create mode 100755 internal/policies/certificate/cert-autoenroll create mode 100644 internal/policies/certificate/cert-autoenroll_test.go create mode 100644 internal/policies/certificate/certificate.go create mode 100644 internal/policies/certificate/certificate_test.go create mode 100644 internal/policies/certificate/python/vendor_samba/gp/gp_cert_auto_enroll_ext.py create mode 100644 internal/policies/certificate/python/vendor_samba/gp/gpclass.py create mode 100644 internal/policies/certificate/python/vendor_samba/gp/util/logging.py create mode 100644 internal/policies/certificate/testdata/TestCertAutoenrollScript/golden/enroll_with_empty_advanced_configuration create mode 100644 internal/policies/certificate/testdata/TestCertAutoenrollScript/golden/enroll_with_simple_configuration create mode 100644 internal/policies/certificate/testdata/TestCertAutoenrollScript/golden/enroll_with_simple_configuration_and_debug_enabled create mode 100644 internal/policies/certificate/testdata/TestCertAutoenrollScript/golden/enroll_with_valid_advanced_configuration create mode 100644 internal/policies/certificate/testdata/TestCertAutoenrollScript/golden/unenroll create mode 100644 internal/policies/certificate/testdata/TestPolicyApply/golden/computer,_configured_to_enroll create mode 100644 internal/policies/certificate/testdata/TestPolicyApply/golden/computer,_configured_to_enroll,_advanced_configuration create mode 100644 internal/policies/certificate/testdata/TestPolicyApply/golden/computer,_configured_to_unenroll create mode 100644 internal/testutils/admock/samba/dcerpc/preg.py create mode 100644 internal/testutils/admock/vendor_samba/__init__.py create mode 100644 internal/testutils/admock/vendor_samba/gp/__init__.py create mode 100644 internal/testutils/admock/vendor_samba/gp/gp_cert_auto_enroll_ext.py create mode 100644 internal/testutils/admock/vendor_samba/gp/gpclass.py diff --git a/internal/consts/consts.go b/internal/consts/consts.go index c44f66d56..e53cb0017 100644 --- a/internal/consts/consts.go +++ b/internal/consts/consts.go @@ -21,9 +21,15 @@ const ( // DefaultCacheDir is the default path for adsys system cache directory. DefaultCacheDir = "/var/cache/adsys" + // DefaultStateDir is the default path for adsys system state directory. + DefaultStateDir = "/var/lib/adsys" + // DefaultRunDir is the default path for adsys run directory. DefaultRunDir = "/run/adsys" + // DefaultShareDir is the default path for adsys share directory. + DefaultShareDir = "/usr/share/adsys" + // DefaultClientTimeout is the maximum default time in seconds between 2 server activities before the client returns and abort the request. DefaultClientTimeout = 30 diff --git a/internal/policies/certificate/cert-autoenroll b/internal/policies/certificate/cert-autoenroll new file mode 100755 index 000000000..02e0a3a35 --- /dev/null +++ b/internal/policies/certificate/cert-autoenroll @@ -0,0 +1,131 @@ +#!/usr/bin/python3 + +import argparse +import json +import os +import sys +import tempfile + +from collections import namedtuple + +from samba import param +from samba.credentials import MUST_USE_KERBEROS, Credentials +from samba.dcerpc import preg + +from vendor_samba.gp.gpclass import GPOStorage +from vendor_samba.gp import gp_cert_auto_enroll_ext as cae + +class adsys_cert_auto_enroll(cae.gp_cert_auto_enroll_ext): + def enroll(self, guid, entries, trust_dir, private_dir, global_trust_dir): + self._gp_cert_auto_enroll_ext__enroll(guid, entries, trust_dir, private_dir, global_trust_dir) + + def unenroll(self, guid): + ca_attrs = self.cache_get_all_attribute_values(guid) + self.clean(guid, remove=list(ca_attrs.keys())) + +def smb_config(realm, enable_debug): + config = "[global]\nrealm = %s\n" % realm + if enable_debug: + config += "log level = 10\n" + return config + +def main(): + parser = argparse.ArgumentParser(description='Certificate autoenrollment via Samba') + parser.add_argument('action', type=str, + help='Action to perform (one of: enroll, unenroll)', + choices=['enroll', 'unenroll']) + parser.add_argument('object_name', type=str, + help='The computer name to enroll/unenroll, e.g. keypress') + parser.add_argument('realm', type=str, + help='The realm of the domain, e.g. example.com') + + parser.add_argument('--policy_servers_json', type=str, + help='GPO entries for advanced configuration of the policy servers. \ + Must be in JSON format.') + parser.add_argument('--state_dir', type=str, + default='/var/lib/adsys', + help='Directory to store GPO state in.') + parser.add_argument('--private_dir', type=str, + default='/var/lib/adsys/private/certs', + help='Directory to store private keys in.') + parser.add_argument('--trust_dir', type=str, + default='/var/lib/adsys/certs', + help='Directory to store trusted certificates in.') + parser.add_argument('--global_trust_dir', type=str, + default='/usr/local/share/ca-certificates', + help='Directory to symlink root CA certificates to.') + parser.add_argument('--debug', action='store_true', + help='Enable samba debug output.') + + args = parser.parse_args() + + state_dir = args.state_dir + trust_dir = args.trust_dir + private_dir = args.private_dir + global_trust_dir = args.global_trust_dir + + # Create needed directories if they don't exist + for directory in [state_dir, trust_dir, private_dir]: + if not os.path.exists(directory): + os.makedirs(directory) + + with tempfile.NamedTemporaryFile(prefix='smb_conf') as smb_conf: + smb_conf.write(smb_config(args.realm, args.debug).encode('utf-8')) + smb_conf.flush() + + lp = param.LoadParm(smb_conf.name) + c = Credentials() + c.set_kerberos_state(MUST_USE_KERBEROS) + c.guess(lp) + username = c.get_username() + store = GPOStorage(os.path.join(state_dir, f'cert_gpo_state_{args.object_name}.tdb')) + + ext = adsys_cert_auto_enroll(lp, c, username, store) + guid = f'adsys-cert-autoenroll-{args.object_name}' + if args.action == 'enroll': + entries = gpo_entries(args.policy_servers_json) + ext.enroll(guid, entries, trust_dir, private_dir, global_trust_dir) + else: + ext.unenroll(guid) + +def gpo_entries(entries_json): + """ + Convert JSON string to list of GPO entries + + JSON must be an array of objects with the following keys: + keyname (str): Registry key name + valuename (str): Registry value name + type (int): Registry value type + data (any): Registry value data + + Parameters: + entries_json (str): JSON string of GPO entries + Returns: + list: List of GPO entries, or empty list if entries_json is empty + """ + + if not entries_json: + return [] + + entries_dict = json.loads(entries_json) + if not entries_dict: + return [] + + entries = [] + for entry in entries_dict: + try: + e = preg.entry() + e.keyname = entry['keyname'] + e.valuename = entry['valuename'] + e.type = entry['type'] + e.data = entry['data'] + entries.append(e) + except KeyError as exc: + raise ValueError(f'Could not find key {exc} in GPO entry') from exc + except TypeError as exc: + raise ValueError(f'GPO data must be a JSON array of objects') from exc + return entries + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/internal/policies/certificate/cert-autoenroll_test.go b/internal/policies/certificate/cert-autoenroll_test.go new file mode 100644 index 000000000..8899c35d9 --- /dev/null +++ b/internal/policies/certificate/cert-autoenroll_test.go @@ -0,0 +1,142 @@ +package certificate_test + +import ( + "bytes" + "encoding/json" + "os" + "os/exec" + "path/filepath" + "strings" + "testing" + + "github.com/stretchr/testify/require" + "github.com/ubuntu/adsys/internal/testutils" +) + +const advancedConfigurationJSON = `[ + { + "keyname": "Software\\Policies\\Microsoft\\Cryptography\\PolicyServers\\37c9dc30f207f27f61a2f7c3aed598a6e2920b54", + "valuename": "AuthFlags", + "data": 2, + "type": 4 + }, + { + "keyname": "Software\\Policies\\Microsoft\\Cryptography\\PolicyServers\\37c9dc30f207f27f61a2f7c3aed598a6e2920b54", + "valuename": "Cost", + "data": 2147483645, + "type": 4 + }, + { + "keyname": "Software\\Policies\\Microsoft\\Cryptography\\PolicyServers\\37c9dc30f207f27f61a2f7c3aed598a6e2920b54", + "valuename": "Flags", + "data": 20, + "type": 4 + }, + { + "keyname": "Software\\Policies\\Microsoft\\Cryptography\\PolicyServers\\37c9dc30f207f27f61a2f7c3aed598a6e2920b54", + "valuename": "FriendlyName", + "data": "ActiveDirectoryEnrollmentPolicy", + "type": 1 + }, + { + "keyname": "Software\\Policies\\Microsoft\\Cryptography\\PolicyServers\\37c9dc30f207f27f61a2f7c3aed598a6e2920b54", + "valuename": "PolicyID", + "data": "{A5E9BF57-71C6-443A-B7FC-79EFA6F73EBD}", + "type": 1 + }, + { + "keyname": "Software\\Policies\\Microsoft\\Cryptography\\PolicyServers\\37c9dc30f207f27f61a2f7c3aed598a6e2920b54", + "valuename": "URL", + "data": "LDAP:", + "type": 1 + }, + { + "keyname": "Software\\Policies\\Microsoft\\Cryptography\\PolicyServers", + "valuename": "Flags", + "data": 0, + "type": 4 + } +]` + +func TestCertAutoenrollScript(t *testing.T) { + t.Parallel() + + coverageOn := testutils.PythonCoverageToGoFormat(t, "cert-autoenroll", false) + certAutoenrollCmd := "./cert-autoenroll" + if coverageOn { + certAutoenrollCmd = "cert-autoenroll" + } + + compactedJSON := &bytes.Buffer{} + err := json.Compact(compactedJSON, []byte(advancedConfigurationJSON)) + require.NoError(t, err, "Failed to compact JSON") + + // Setup samba mock + pythonPath, err := filepath.Abs("../../testutils/admock") + require.NoError(t, err, "Setup: Failed to get current absolute path for mock") + + tests := map[string]struct { + args []string + + readOnlyPath bool + autoenrollError bool + + wantErr bool + }{ + "Enroll with simple configuration": {args: []string{"enroll", "keypress", "example.com"}}, + "Enroll with simple configuration and debug enabled": {args: []string{"enroll", "keypress", "example.com", "--debug"}}, + "Enroll with empty advanced configuration": {args: []string{"enroll", "keypress", "example.com", "--policy_servers_json", "null"}}, + "Enroll with valid advanced configuration": {args: []string{"enroll", "keypress", "example.com", "--policy_servers_json", compactedJSON.String()}}, + + "Unenroll": {args: []string{"unenroll", "keypress", "example.com"}}, + + // Error cases + "Error on missing arguments": {args: []string{"enroll"}, wantErr: true}, + "Error on invalid flags": {args: []string{"enroll", "keypress", "example.com", "--invalid_flag"}, wantErr: true}, + "Error on invalid JSON": {args: []string{"enroll", "keypress", "example.com", "--policy_servers_json", "invalid_json"}, wantErr: true}, + "Error on invalid JSON keys": { + args: []string{"enroll", "keypress", "example.com", "--policy_servers_json", `[{"key":"Software\\Policies\\Microsoft","value":"MyValue"}]`}, wantErr: true}, + "Error on invalid JSON structure": { + args: []string{"enroll", "keypress", "example.com", "--policy_servers_json", `{"key":"Software\\Policies\\Microsoft","value":"MyValue"}`}, wantErr: true}, + "Error on read-only path": {readOnlyPath: true, args: []string{"enroll", "keypress", "example.com"}, wantErr: true}, + "Error on enroll failure": {autoenrollError: true, args: []string{"enroll", "keypress", "example.com"}, wantErr: true}, + "Error on unenroll failure": {autoenrollError: true, args: []string{"unenroll", "keypress", "example.com"}, wantErr: true}, + } + + for name, tc := range tests { + tc := tc + name := name + t.Run(name, func(t *testing.T) { + t.Parallel() + + tmpdir := t.TempDir() + stateDir := filepath.Join(tmpdir, "state") + privateDir := filepath.Join(tmpdir, "private") + trustDir := filepath.Join(tmpdir, "trust") + globalTrustDir := filepath.Join(tmpdir, "ca-certificates") + + if tc.readOnlyPath { + testutils.MakeReadOnly(t, tmpdir) + } + + args := append(tc.args, "--state_dir", stateDir, "--private_dir", privateDir, "--trust_dir", trustDir, "--global_trust_dir", globalTrustDir) + + // #nosec G204: we control the command line name and only change it for tests + cmd := exec.Command(certAutoenrollCmd, args...) + cmd.Env = append(os.Environ(), "PYTHONPATH="+pythonPath) + if tc.autoenrollError { + cmd.Env = append(os.Environ(), "ADSYS_WANT_AUTOENROLL_ERROR=1") + } + out, err := cmd.CombinedOutput() + if tc.wantErr { + require.Error(t, err, "cert-autoenroll should have failed but didn’t") + return + } + require.NoErrorf(t, err, "cert-autoenroll should have exited successfully: %s", string(out)) + + got := strings.ReplaceAll(string(out), tmpdir, "#TMPDIR#") + want := testutils.LoadWithUpdateFromGolden(t, got) + require.Equal(t, want, got, "Unexpected output from cert-autoenroll script") + }) + } +} diff --git a/internal/policies/certificate/certificate.go b/internal/policies/certificate/certificate.go new file mode 100644 index 000000000..a1a05507d --- /dev/null +++ b/internal/policies/certificate/certificate.go @@ -0,0 +1,259 @@ +// Package certificate provides a manager that handles certificate +// autoenrollment. +// +// This manager only applies to computer objects. +// +// Provided that the AD backend is online and AD CS is set up, the manager will +// parse the relevant GPOs and delegate to an external Python script that will +// request Samba to enroll or un-enroll the machine for certificates. +// +// No action is taken if the certificate GPO is disabled, not configured, or +// absent. +// If the enroll flag is not set, the machine will be un-enrolled, +// namely the certificates will be removed and monitoring will stop. +// If any errors occur during the enrollment process, the manager will log them +// prior to failing. +package certificate + +import ( + "bytes" + "context" + _ "embed" // embed cert enroll python script + "encoding/json" + "fmt" + "os" + "os/exec" + "path/filepath" + "strconv" + "strings" + "sync" + "time" + + "github.com/ubuntu/adsys/internal/consts" + log "github.com/ubuntu/adsys/internal/grpc/logstreamer" + "github.com/ubuntu/adsys/internal/i18n" + "github.com/ubuntu/adsys/internal/policies/entry" + "github.com/ubuntu/adsys/internal/smbsafe" + "github.com/ubuntu/decorate" + "golang.org/x/exp/slices" +) + +// Manager prevents running multiple Python scripts in parallel while parsing +// the policy in ApplyPolicy. +type Manager struct { + domain string + stateDir string + krb5CacheDir string + vendorPythonDir string + certEnrollCmd []string + + mu sync.Mutex // Prevents multiple instances of the certificate manager from running in parallel +} + +// gpoEntry is a single GPO registry entry to be serialised to JSON in a format +// Samba expects. +type gpoEntry struct { + KeyName string `json:"keyname"` + ValueName string `json:"valuename"` + Data any `json:"data"` + Type int `json:"type"` +} + +// integerGPOValues is a list of GPO registry values that contain integer data. +var integerGPOValues = []string{"AuthFlags", "Cost", "Flags"} + +const ( + gpoTypeString int = 1 // REG_SZ + gpoTypeInteger int = 4 // REG_DWORD + + // See [MS-CAESO] 4.4.5.1. + enrollFlag int = 0x1 + disabledFlag int = 0x8000 +) + +// CertEnrollCode is the embedded Python script which requests +// Samba to autoenroll for certificates using the given GPOs. +// +//go:embed cert-autoenroll +var CertEnrollCode string + +type options struct { + stateDir string + runDir string + shareDir string + certAutoenrollCmd []string +} + +// Option reprents an optional function to change the certificate manager. +type Option func(*options) + +// WithStateDir overrides the default state directory. +func WithStateDir(p string) func(*options) { + return func(a *options) { + a.stateDir = p + } +} + +// WithRunDir overrides the default run directory. +func WithRunDir(p string) func(*options) { + return func(a *options) { + a.runDir = p + } +} + +// WithShareDir overrides the default share directory. +func WithShareDir(p string) func(*options) { + return func(a *options) { + a.shareDir = p + } +} + +// WithCertAutoenrollCmd overrides the default certificate autoenroll command. +func WithCertAutoenrollCmd(cmd []string) func(*options) { + return func(a *options) { + a.certAutoenrollCmd = cmd + } +} + +// New returns a new manager for the certificate policy. +func New(domain string, opts ...Option) *Manager { + // defaults + args := options{ + stateDir: consts.DefaultStateDir, + runDir: consts.DefaultRunDir, + shareDir: consts.DefaultShareDir, + certAutoenrollCmd: []string{"python3", "-c", CertEnrollCode}, + } + // applied options + for _, o := range opts { + o(&args) + } + + return &Manager{ + domain: domain, + stateDir: args.stateDir, + krb5CacheDir: filepath.Join(args.runDir, "krb5cc"), + vendorPythonDir: filepath.Join(args.shareDir, "python"), + certEnrollCmd: args.certAutoenrollCmd, + } +} + +// ApplyPolicy runs the certificate autoenrollment script to enroll or un-enroll the machine. +func (m *Manager) ApplyPolicy(ctx context.Context, objectName string, isComputer, isOnline bool, entries []entry.Entry) (err error) { + defer decorate.OnError(&err, i18n.G("can't apply certificate policy")) + + m.mu.Lock() + defer m.mu.Unlock() + + idx := slices.IndexFunc(entries, func(e entry.Entry) bool { return e.Key == "autoenroll" }) + if idx == -1 { + log.Debug(ctx, "Certificate autoenrollment is not configured") + return nil + } + + if !isComputer { + log.Debug(ctx, "Certificate policy is only supported for computers, skipping...") + return nil + } + + if !isOnline { + log.Info(ctx, i18n.G("AD backend is offline, skipping certificate policy")) + return nil + } + + log.Debug(ctx, "ApplyPolicy certificate policy") + + entry := entries[idx] + value, err := strconv.Atoi(entry.Value) + if err != nil { + return fmt.Errorf(i18n.G("failed to parse certificate policy entry value: %w"), err) + } + + if value&disabledFlag == disabledFlag { + log.Debug(ctx, "Certificate policy is disabled, skipping...") + return nil + } + + var polSrvRegistryEntries []gpoEntry + for _, entry := range entries { + // We already handled the autoenroll entry + if entry.Key == "autoenroll" { + continue + } + + // Samba expects the key parts to be joined by backslashes + keyparts := strings.Split(entry.Key, "/") + keyname := strings.Join(keyparts[:len(keyparts)-1], `\`) + valuename := keyparts[len(keyparts)-1] + polSrvRegistryEntries = append(polSrvRegistryEntries, gpoEntry{keyname, valuename, gpoData(entry.Value, valuename), gpoType(valuename)}) + + log.Debugf(ctx, "Certificate policy entry: %#v", entry) + } + + var action string + log.Debugf(ctx, "Certificate policy value: %d", value) + action = "unenroll" + if value&enrollFlag == enrollFlag { + action = "enroll" + } + + jsonGPOData, err := json.Marshal(polSrvRegistryEntries) + if err != nil { + return fmt.Errorf(i18n.G("failed to marshal policy server registry entries: %v"), err) + } + + if err := m.runScript(ctx, action, objectName, + "--policy_servers_json", string(jsonGPOData), + "--state_dir", m.stateDir, + ); err != nil { + return err + } + + return nil +} + +// runScript runs the certificate autoenrollment script with the given arguments. +func (m *Manager) runScript(ctx context.Context, action, objectName string, extraArgs ...string) error { + scriptArgs := []string{action, objectName, m.domain} + scriptArgs = append(scriptArgs, extraArgs...) + cmdArgs := append(m.certEnrollCmd, scriptArgs...) + cmdCtx, cancel := context.WithTimeout(ctx, time.Second*10) + defer cancel() + log.Debugf(ctx, "Running cert autoenroll script with arguments: %q", strings.Join(scriptArgs, " ")) + // #nosec G204 - cmdArgs is under our control (python embedded script or mock for tests) + cmd := exec.CommandContext(cmdCtx, cmdArgs[0], cmdArgs[1:]...) + cmd.Env = append(os.Environ(), + fmt.Sprintf("KRB5CCNAME=%s", filepath.Join(m.krb5CacheDir, objectName)), + fmt.Sprintf("PYTHONPATH=%s", m.vendorPythonDir), + ) + var stdout, stderr bytes.Buffer + cmd.Stdout = &stdout + cmd.Stderr = &stderr + smbsafe.WaitExec() + defer smbsafe.DoneExec() + + if err := cmd.Run(); err != nil { + return fmt.Errorf(i18n.G("failed to run certificate autoenrollment script (exited with %d): %v\n%s"), cmd.ProcessState.ExitCode(), err, stderr.String()) + } + log.Infof(ctx, i18n.G("Certificate autoenrollment script ran successfully\n%s"), stdout.String()) + return nil +} + +// gpoData returns the data for a GPO entry. +func gpoData(data, value string) any { + if slices.Contains(integerGPOValues, value) { + intData, _ := strconv.Atoi(data) + return intData + } + + return data +} + +// gpoType returns the type for a GPO entry. +func gpoType(value string) int { + if slices.Contains(integerGPOValues, value) { + return gpoTypeInteger + } + + return gpoTypeString +} diff --git a/internal/policies/certificate/certificate_test.go b/internal/policies/certificate/certificate_test.go new file mode 100644 index 000000000..56fddffb5 --- /dev/null +++ b/internal/policies/certificate/certificate_test.go @@ -0,0 +1,154 @@ +package certificate_test + +import ( + "context" + "flag" + "fmt" + "os" + "path/filepath" + "strings" + "testing" + + "github.com/stretchr/testify/require" + "github.com/ubuntu/adsys/internal/policies/certificate" + "github.com/ubuntu/adsys/internal/policies/entry" + "github.com/ubuntu/adsys/internal/testutils" +) + +const ( + enrollValue = "7" // string representation of 0b111 + unenrollValue = "6" // string representation of 0b110 + disabledValue = "32768" // string representation of 0x8000 +) + +var enrollEntry = entry.Entry{Key: "autoenroll", Value: enrollValue} +var advancedConfigurationEntries = []entry.Entry{ + {Key: "Software/Policies/Microsoft/Cryptography/PolicyServers/37c9dc30f207f27f61a2f7c3aed598a6e2920b54/AuthFlags", Value: "2"}, + {Key: "Software/Policies/Microsoft/Cryptography/PolicyServers/37c9dc30f207f27f61a2f7c3aed598a6e2920b54/Cost", Value: "2147483645"}, + {Key: "Software/Policies/Microsoft/Cryptography/PolicyServers/37c9dc30f207f27f61a2f7c3aed598a6e2920b54/Flags", Value: "20"}, + {Key: "Software/Policies/Microsoft/Cryptography/PolicyServers/37c9dc30f207f27f61a2f7c3aed598a6e2920b54/FriendlyName", Value: "ActiveDirectoryEnrollmentPolicy"}, + {Key: "Software/Policies/Microsoft/Cryptography/PolicyServers/37c9dc30f207f27f61a2f7c3aed598a6e2920b54/PolicyID", Value: "{A5E9BF57-71C6-443A-B7FC-79EFA6F73EBD}"}, + {Key: "Software/Policies/Microsoft/Cryptography/PolicyServers/37c9dc30f207f27f61a2f7c3aed598a6e2920b54/URL", Value: "LDAP:"}, + {Key: "Software/Policies/Microsoft/Cryptography/PolicyServers/Flags", Value: "0"}, +} + +func TestPolicyApply(t *testing.T) { + t.Parallel() + + tests := map[string]struct { + entries []entry.Entry + + isUser bool + isOffline bool + + autoenrollScriptError bool + runScript bool + + wantErr bool + }{ + "Computer, no entries": {}, + "Computer, configured to enroll": {entries: []entry.Entry{enrollEntry}, runScript: true}, + "Computer, configured to enroll, advanced configuration": {entries: append(advancedConfigurationEntries, enrollEntry), runScript: true}, + "Computer, configured to unenroll": {entries: []entry.Entry{{Key: "autoenroll", Value: unenrollValue}}, runScript: true}, + "Computer, autoenroll disabled": {entries: []entry.Entry{{Key: "autoenroll", Value: disabledValue}}}, + "Computer, domain is offline": {entries: []entry.Entry{enrollEntry}, isOffline: true}, + + "User, autoenroll not supported": {isUser: true, entries: []entry.Entry{enrollEntry}}, + + // Error cases + "Error on autoenroll script failure": {autoenrollScriptError: true, entries: []entry.Entry{enrollEntry}, wantErr: true}, + "Error on invalid autoenroll value": {entries: []entry.Entry{{Key: "autoenroll", Value: "notanumber"}}, wantErr: true}, + } + + for name, tc := range tests { + tc := tc + t.Run(name, func(t *testing.T) { + t.Parallel() + + tmpdir := t.TempDir() + autoenrollCmdOutputFile := filepath.Join(tmpdir, "autoenroll-output") + autoenrollCmd := mockAutoenrollScript(t, autoenrollCmdOutputFile, tc.autoenrollScriptError) + + m := certificate.New( + "example.com", + certificate.WithStateDir(filepath.Join(tmpdir, "statedir")), + certificate.WithRunDir(filepath.Join(tmpdir, "rundir")), + certificate.WithShareDir(filepath.Join(tmpdir, "sharedir")), + certificate.WithCertAutoenrollCmd(autoenrollCmd), + ) + + err := m.ApplyPolicy(context.Background(), "keypress", !tc.isUser, !tc.isOffline, tc.entries) + if tc.wantErr { + require.Error(t, err, "ApplyPolicy should fail") + return + } + require.NoError(t, err, "ApplyPolicy should succeed") + + // Check that the autoenroll script was called with the expected arguments + // and that the output file was created + if !tc.runScript { + return + } + + got, err := os.ReadFile(autoenrollCmdOutputFile) + require.NoError(t, err, "Setup: Autoenroll mock output should be readable") + + want := testutils.LoadWithUpdateFromGolden(t, string(got)) + require.Equal(t, want, string(got), "Unexpected output from autoenroll mock") + }) + } +} + +func mockAutoenrollScript(t *testing.T, scriptOutputFile string, autoenrollScriptError bool) []string { + t.Helper() + + cmdArgs := []string{"env", "GO_WANT_HELPER_PROCESS=1", os.Args[0], "-test.run=TestMockAutoenrollScript", "--", scriptOutputFile} + if autoenrollScriptError { + cmdArgs = append(cmdArgs, "-Exit1-") + } + + return cmdArgs +} + +func TestMockAutoenrollScript(t *testing.T) { + if os.Getenv("GO_WANT_HELPER_PROCESS") != "1" { + return + } + defer os.Exit(0) + + var outputFile string + + args := os.Args + for len(args) > 0 { + if args[0] == "--" { + outputFile = args[1] + args = args[2:] + break + } + args = args[1:] + } + + if args[0] == "-Exit1-" { + fmt.Fprintf(os.Stderr, "EXIT 1 requested in mock") + os.Exit(1) + } + + dataToWrite := strings.Join(args, " ") + "\n" + dataToWrite += "KRB5CCNAME=" + os.Getenv("KRB5CCNAME") + "\n" + dataToWrite += "PYTHONPATH=" + os.Getenv("PYTHONPATH") + "\n" + + // Replace tmpdir with a placeholder to avoid non-deterministic test failures + tmpdir := filepath.Dir(outputFile) + dataToWrite = strings.ReplaceAll(dataToWrite, tmpdir, "#TMPDIR#") + + err := os.WriteFile(outputFile, []byte(dataToWrite), 0600) + require.NoError(t, err, "Setup: Can't write script args to output file") +} + +func TestMain(m *testing.M) { + testutils.InstallUpdateFlag() + flag.Parse() + + m.Run() + testutils.MergeCoverages() +} diff --git a/internal/policies/certificate/python/vendor_samba/gp/gp_cert_auto_enroll_ext.py b/internal/policies/certificate/python/vendor_samba/gp/gp_cert_auto_enroll_ext.py new file mode 100644 index 000000000..54be3bc28 --- /dev/null +++ b/internal/policies/certificate/python/vendor_samba/gp/gp_cert_auto_enroll_ext.py @@ -0,0 +1,536 @@ +# gp_cert_auto_enroll_ext samba group policy +# Copyright (C) David Mulder 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 . + +import os +import operator +import requests +from vendor_samba.gp.gpclass import gp_pol_ext, gp_applier, GPOSTATE +from samba import Ldb +from ldb import SCOPE_SUBTREE, SCOPE_BASE +from samba.auth import system_session +from vendor_samba.gp.gpclass import get_dc_hostname +import base64 +from shutil import which +from subprocess import Popen, PIPE +import re +import json +from vendor_samba.gp.util.logging import log +import struct +try: + from cryptography.hazmat.primitives.serialization.pkcs7 import \ + load_der_pkcs7_certificates +except ModuleNotFoundError: + def load_der_pkcs7_certificates(x): return [] + log.error('python cryptography missing pkcs7 support. ' + 'Certificate chain parsing will fail') +from cryptography.hazmat.primitives.serialization import Encoding +from cryptography.x509 import load_der_x509_certificate +from cryptography.hazmat.backends import default_backend +from samba.common import get_string + +cert_wrap = b""" +-----BEGIN CERTIFICATE----- +%s +-----END CERTIFICATE-----""" +endpoint_re = '(https|HTTPS)://(?P[a-zA-Z0-9.-]+)/ADPolicyProvider' + \ + '_CEP_(?P[a-zA-Z]+)/service.svc/CEP' + + +def octet_string_to_objectGUID(data): + """Convert an octet string to an objectGUID.""" + return '%s-%s-%s-%s-%s' % ('%02x' % struct.unpack('H', data[8:10])[0], + '%02x%02x' % struct.unpack('>HL', data[10:])) + + +def group_and_sort_end_point_information(end_point_information): + """Group and Sort End Point Information. + + [MS-CAESO] 4.4.5.3.2.3 + In this step autoenrollment processes the end point information by grouping + it by CEP ID and sorting in the order with which it will use the end point + to access the CEP information. + """ + # Create groups of the CertificateEnrollmentPolicyEndPoint instances that + # have the same value of the EndPoint.PolicyID datum. + end_point_groups = {} + for e in end_point_information: + if e['PolicyID'] not in end_point_groups.keys(): + end_point_groups[e['PolicyID']] = [] + end_point_groups[e['PolicyID']].append(e) + + # Sort each group by following these rules: + for end_point_group in end_point_groups.values(): + # Sort the CertificateEnrollmentPolicyEndPoint instances in ascending + # order based on the EndPoint.Cost value. + end_point_group.sort(key=lambda e: e['Cost']) + + # For instances that have the same EndPoint.Cost: + cost_list = [e['Cost'] for e in end_point_group] + costs = set(cost_list) + for cost in costs: + i = cost_list.index(cost) + j = len(cost_list)-operator.indexOf(reversed(cost_list), cost)-1 + if i == j: + continue + + # Sort those that have EndPoint.Authentication equal to Kerberos + # first. Then sort those that have EndPoint.Authentication equal to + # Anonymous. The rest of the CertificateEnrollmentPolicyEndPoint + # instances follow in an arbitrary order. + def sort_auth(e): + # 0x2 - Kerberos + if e['AuthFlags'] == 0x2: + return 0 + # 0x1 - Anonymous + elif e['AuthFlags'] == 0x1: + return 1 + else: + return 2 + end_point_group[i:j+1] = sorted(end_point_group[i:j+1], + key=sort_auth) + return list(end_point_groups.values()) + +def obtain_end_point_information(entries): + """Obtain End Point Information. + + [MS-CAESO] 4.4.5.3.2.2 + In this step autoenrollment initializes the + CertificateEnrollmentPolicyEndPoints table. + """ + end_point_information = {} + section = 'Software\\Policies\\Microsoft\\Cryptography\\PolicyServers\\' + for e in entries: + if not e.keyname.startswith(section): + continue + name = e.keyname.replace(section, '') + if name not in end_point_information.keys(): + end_point_information[name] = {} + end_point_information[name][e.valuename] = e.data + for ca in end_point_information.values(): + m = re.match(endpoint_re, ca['URL']) + if m: + name = '%s-CA' % m.group('server').replace('.', '-') + ca['name'] = name + ca['hostname'] = m.group('server') + ca['auth'] = m.group('auth') + elif ca['URL'].lower() != 'ldap:': + edata = { 'endpoint': ca['URL'] } + log.error('Failed to parse the endpoint', edata) + return {} + end_point_information = \ + group_and_sort_end_point_information(end_point_information.values()) + return end_point_information + +def fetch_certification_authorities(ldb): + """Initialize CAs. + + [MS-CAESO] 4.4.5.3.1.2 + """ + result = [] + basedn = ldb.get_default_basedn() + # Autoenrollment MUST do an LDAP search for the CA information + # (pKIEnrollmentService) objects under the following container: + dn = 'CN=Enrollment Services,CN=Public Key Services,CN=Services,CN=Configuration,%s' % basedn + attrs = ['cACertificate', 'cn', 'dNSHostName'] + expr = '(objectClass=pKIEnrollmentService)' + res = ldb.search(dn, SCOPE_SUBTREE, expr, attrs) + if len(res) == 0: + return result + for es in res: + data = { 'name': get_string(es['cn'][0]), + 'hostname': get_string(es['dNSHostName'][0]), + 'cACertificate': get_string(base64.b64encode(es['cACertificate'][0])) + } + result.append(data) + return result + +def fetch_template_attrs(ldb, name, attrs=None): + if attrs is None: + 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): + return cert_wrap % re.sub(b"(.{64})", b"\\1\n", cert.encode(), 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' + p = Popen([cepces_submit, '--server=%s' % server, '--auth=Kerberos'], + env=env, stdout=PIPE, stderr=PIPE) + out, err = p.communicate() + if p.returncode != 0: + data = { 'Error': err.decode() } + log.error('Failed to fetch the list of supported templates.', data) + return out.strip().split() + return [] + + +def getca(ca, url, trust_dir): + """Fetch Certificate Chain from the CA.""" + root_cert = os.path.join(trust_dir, '%s.crt' % ca['name']) + root_certs = [] + + try: + r = requests.get(url=url, params={'operation': 'GetCACert', + 'message': 'CAIdentifier'}) + except requests.exceptions.ConnectionError: + log.warn('Failed to establish a new connection') + r = None + if r is None or r.content == b'' or r.headers['Content-Type'] == 'text/html': + log.warn('Failed to fetch the root certificate chain.') + log.warn('The Network Device Enrollment Service is either not' + + ' installed or not configured.') + if 'cACertificate' in ca: + log.warn('Installing the server certificate only.') + try: + cert = load_der_x509_certificate(ca['cACertificate']) + except TypeError: + cert = load_der_x509_certificate(ca['cACertificate'], + default_backend()) + cert_data = cert.public_bytes(Encoding.PEM) + with open(root_cert, 'wb') as w: + w.write(cert_data) + root_certs.append(root_cert) + return root_certs + + if r.headers['Content-Type'] == 'application/x-x509-ca-cert': + # Older versions of load_der_x509_certificate require a backend param + try: + cert = load_der_x509_certificate(r.content) + except TypeError: + cert = load_der_x509_certificate(r.content, default_backend()) + cert_data = cert.public_bytes(Encoding.PEM) + with open(root_cert, 'wb') as w: + w.write(cert_data) + root_certs.append(root_cert) + elif r.headers['Content-Type'] == 'application/x-x509-ca-ra-cert': + certs = load_der_pkcs7_certificates(r.content) + for i in range(0, len(certs)): + cert = certs[i].public_bytes(Encoding.PEM) + filename, extension = root_cert.rsplit('.', 1) + dest = '%s.%d.%s' % (filename, i, extension) + with open(dest, 'wb') as w: + w.write(cert) + root_certs.append(dest) + else: + log.warn('getca: Wrong (or missing) MIME content type') + + return root_certs + + +def cert_enroll(ca, ldb, trust_dir, private_dir, global_trust_dir, auth='Kerberos'): + """Install the root certificate chain.""" + data = dict({'files': [], 'templates': []}, **ca) + url = 'http://%s/CertSrv/mscep/mscep.dll/pkiclient.exe?' % ca['hostname'] + root_certs = getca(ca, url, trust_dir) + 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: + log.warn('Failed to symlink root certificate to the' + ' admin trust anchors') + except FileNotFoundError: + log.warn('Failed to symlink root certificate to the' + ' admin trust anchors.' + ' The directory was not found', global_trust_dir) + except FileExistsError: + # If we're simply downloading a renewed cert, the symlink + # already exists. Ignore the FileExistsError. Preserve the + # existing symlink in the unapply data. + data['files'].append(dst) + 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): + p = Popen([getcert, 'add-ca', '-c', ca['name'], '-e', + '%s --server=%s --auth=%s' % (cepces_submit, + ca['hostname'], auth)], + stdout=PIPE, stderr=PIPE) + out, err = p.communicate() + log.debug(out.decode()) + if p.returncode != 0: + data = { 'Error': err.decode(), 'CA': ca['name'] } + log.error('Failed to add Certificate Authority', data) + supported_templates = get_supported_templates(ca['hostname']) + for template in supported_templates: + attrs = fetch_template_attrs(ldb, template) + nickname = '%s.%s' % (ca['name'], template.decode()) + keyfile = os.path.join(private_dir, '%s.key' % nickname) + certfile = os.path.join(trust_dir, '%s.crt' % nickname) + p = Popen([getcert, 'request', '-c', ca['name'], + '-T', template.decode(), + '-I', nickname, '-k', keyfile, '-f', certfile, + '-g', attrs['msPKI-Minimal-Key-Size'][0]], + stdout=PIPE, stderr=PIPE) + out, err = p.communicate() + log.debug(out.decode()) + if p.returncode != 0: + data = { 'Error': err.decode(), 'Certificate': nickname } + log.error('Failed to request certificate', data) + data['files'].extend([keyfile, certfile]) + data['templates'].append(nickname) + if update is not None: + Popen([update]).wait() + else: + log.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, gp_applier): + def __str__(self): + return 'Cryptography\AutoEnrollment' + + def unapply(self, guid, attribute, value): + ca_cn = base64.b64decode(attribute) + data = json.loads(value) + 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): + if os.path.exists(f): + os.unlink(f) + self.cache_remove_attribute(guid, attribute) + + def apply(self, guid, ca, applier_func, *args, **kwargs): + attribute = base64.b64encode(ca['name'].encode()).decode() + # If the policy has changed, unapply, then apply new policy + old_val = self.cache_get_attribute_value(guid, attribute) + old_data = json.loads(old_val) if old_val is not None else {} + templates = ['%s.%s' % (ca['name'], t.decode()) for t in get_supported_templates(ca['hostname'])] + new_data = { 'templates': templates, **ca } + if any((new_data[k] != old_data[k] if k in old_data else False) \ + for k in new_data.keys()) or \ + self.cache_get_apply_state() == GPOSTATE.ENFORCE: + self.unapply(guid, attribute, old_val) + # If policy is already applied, skip application + if old_val is not None and \ + self.cache_get_apply_state() != GPOSTATE.ENFORCE: + return + + # Apply the policy and log the changes + data = applier_func(*args, **kwargs) + self.cache_add_attribute(guid, attribute, data) + + def process_group_policy(self, deleted_gpo_list, changed_gpo_list, + trust_dir=None, private_dir=None, global_trust_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 global_trust_dir is None: + global_trust_dir = '/etc/pki/trust/anchors' + 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: + if str(self) in settings: + for ca_cn_enc, data in settings[str(self)].items(): + self.unapply(guid, ca_cn_enc, data) + + for gpo in changed_gpo_list: + if gpo.file_sys_path: + section = 'Software\Policies\Microsoft\Cryptography\AutoEnrollment' + 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 == 0x1 + manage = e.data & 0x2 == 0x2 + retrive_pending = e.data & 0x4 == 0x4 + if enroll: + ca_names = self.__enroll(gpo.name, + pol_conf.entries, + trust_dir, private_dir, + global_trust_dir) + + # Cleanup any old CAs that have been removed + ca_attrs = [base64.b64encode(n.encode()).decode() \ + for n in ca_names] + self.clean(gpo.name, keep=ca_attrs) + else: + # If enrollment has been disabled for this GPO, + # remove any existing policy + ca_attrs = \ + self.cache_get_all_attribute_values(gpo.name) + self.clean(gpo.name, remove=list(ca_attrs.keys())) + + def __read_cep_data(self, guid, ldb, end_point_information, + trust_dir, private_dir, global_trust_dir): + """Read CEP Data. + + [MS-CAESO] 4.4.5.3.2.4 + In this step autoenrollment initializes instances of the + CertificateEnrollmentPolicy by accessing end points associated with CEP + groups created in the previous step. + """ + # For each group created in the previous step: + for end_point_group in end_point_information: + # Pick an arbitrary instance of the + # CertificateEnrollmentPolicyEndPoint from the group + e = end_point_group[0] + + # If this instance does not have the AutoEnrollmentEnabled flag set + # in the EndPoint.Flags, continue with the next group. + if not e['Flags'] & 0x10: + continue + + # If the current group contains a + # CertificateEnrollmentPolicyEndPoint instance with EndPoint.URI + # equal to "LDAP": + if any([e['URL'] == 'LDAP:' for e in end_point_group]): + # Perform an LDAP search to read the value of the objectGuid + # attribute of the root object of the forest root domain NC. If + # any errors are encountered, continue with the next group. + res = ldb.search('', SCOPE_BASE, '(objectClass=*)', + ['rootDomainNamingContext']) + if len(res) != 1: + continue + res2 = ldb.search(res[0]['rootDomainNamingContext'][0], + SCOPE_BASE, '(objectClass=*)', + ['objectGUID']) + if len(res2) != 1: + continue + + # Compare the value read in the previous step to the + # EndPoint.PolicyId datum CertificateEnrollmentPolicyEndPoint + # instance. If the values do not match, continue with the next + # group. + objectGUID = '{%s}' % \ + octet_string_to_objectGUID(res2[0]['objectGUID'][0]).upper() + if objectGUID != e['PolicyID']: + continue + + # For each CertificateEnrollmentPolicyEndPoint instance for that + # group: + ca_names = [] + for ca in end_point_group: + # If EndPoint.URI equals "LDAP": + if ca['URL'] == 'LDAP:': + # This is a basic configuration. + cas = fetch_certification_authorities(ldb) + for _ca in cas: + self.apply(guid, _ca, cert_enroll, _ca, ldb, trust_dir, + private_dir, global_trust_dir) + ca_names.append(_ca['name']) + # If EndPoint.URI starts with "HTTPS//": + elif ca['URL'].lower().startswith('https://'): + self.apply(guid, ca, cert_enroll, ca, ldb, trust_dir, + private_dir, global_trust_dir, auth=ca['auth']) + ca_names.append(ca['name']) + else: + edata = { 'endpoint': ca['URL'] } + log.error('Unrecognized endpoint', edata) + return ca_names + + def __enroll(self, guid, entries, trust_dir, private_dir, global_trust_dir): + url = 'ldap://%s' % get_dc_hostname(self.creds, self.lp) + ldb = Ldb(url=url, session_info=system_session(), + lp=self.lp, credentials=self.creds) + + ca_names = [] + end_point_information = obtain_end_point_information(entries) + if len(end_point_information) > 0: + ca_names.extend(self.__read_cep_data(guid, ldb, + end_point_information, + trust_dir, private_dir, global_trust_dir)) + else: + cas = fetch_certification_authorities(ldb) + for ca in cas: + self.apply(guid, ca, cert_enroll, ca, ldb, trust_dir, + private_dir, global_trust_dir) + ca_names.append(ca['name']) + return ca_names + + 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 == 0x1 + 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) + end_point_information = \ + obtain_end_point_information(pol_conf.entries) + cas = fetch_certification_authorities(ldb) + if len(end_point_information) > 0: + cas2 = [ep for sl in end_point_information for ep in sl] + if any([ca['URL'] == 'LDAP:' for ca in cas2]): + cas.extend(cas2) + else: + cas = cas2 + for ca in cas: + if 'URL' in ca and ca['URL'] == 'LDAP:': + continue + policy = 'Auto Enrollment Policy' + cn = ca['name'] + if policy not in output: + output[policy] = {} + output[policy][cn] = {} + if 'cACertificate' in ca: + output[policy][cn]['CA Certificate'] = \ + format_root_cert(ca['cACertificate']).decode() + output[policy][cn]['Auto Enrollment Server'] = \ + ca['hostname'] + supported_templates = \ + get_supported_templates(ca['hostname']) + output[policy][cn]['Templates'] = \ + [t.decode() for t in supported_templates] + return output diff --git a/internal/policies/certificate/python/vendor_samba/gp/gpclass.py b/internal/policies/certificate/python/vendor_samba/gp/gpclass.py new file mode 100644 index 000000000..0ef86576d --- /dev/null +++ b/internal/policies/certificate/python/vendor_samba/gp/gpclass.py @@ -0,0 +1,884 @@ +# Reads important GPO parameters and updates Samba +# Copyright (C) Luke Morrison 2013 +# +# 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 . + + +import sys +import os, shutil +import errno +import tdb +import pwd +sys.path.insert(0, "bin/python") +from samba import NTSTATUSError +from configparser import ConfigParser +from io import StringIO +import traceback +from samba.common import get_bytes +from abc import ABCMeta, abstractmethod +import xml.etree.ElementTree as etree +import re +from samba.net import Net +from samba.dcerpc import nbt +from samba.samba3 import libsmb_samba_internal as libsmb +import samba.gpo as gpo +from samba.param import LoadParm +from uuid import UUID +from tempfile import NamedTemporaryFile +from samba.dcerpc import preg +from samba.dcerpc import misc +from samba.ndr import ndr_pack, ndr_unpack +from samba.credentials import SMB_SIGNING_REQUIRED +from vendor_samba.gp.util.logging import log +from hashlib import blake2b +import numbers +from samba.common import get_string + +try: + from enum import Enum + GPOSTATE = Enum('GPOSTATE', 'APPLY ENFORCE UNAPPLY') +except ImportError: + class GPOSTATE: + APPLY = 1 + ENFORCE = 2 + UNAPPLY = 3 + + +class gp_log: + ''' Log settings overwritten by gpo apply + The gp_log is an xml file that stores a history of gpo changes (and the + original setting value). + + The log is organized like so: + + + + + + + + + -864000000000 + -36288000000000 + 7 + 1 + + + 1d + + 300 + + + + + + Each guid value contains a list of extensions, which contain a list of + attributes. The guid value represents a GPO. The attributes are the values + of those settings prior to the application of the GPO. + The list of guids is enclosed within a user name, which represents the user + the settings were applied to. This user may be the samaccountname of the + local computer, which implies that these are machine policies. + The applylog keeps track of the order in which the GPOs were applied, so + that they can be rolled back in reverse, returning the machine to the state + prior to policy application. + ''' + def __init__(self, user, gpostore, db_log=None): + ''' Initialize the gp_log + param user - the username (or machine name) that policies are + being applied to + param gpostore - the GPOStorage obj which references the tdb which + contains gp_logs + param db_log - (optional) a string to initialize the gp_log + ''' + self._state = GPOSTATE.APPLY + self.gpostore = gpostore + self.username = user + if db_log: + self.gpdb = etree.fromstring(db_log) + else: + self.gpdb = etree.Element('gp') + self.user = user + user_obj = self.gpdb.find('user[@name="%s"]' % user) + if user_obj is None: + user_obj = etree.SubElement(self.gpdb, 'user') + user_obj.attrib['name'] = user + + def state(self, value): + ''' Policy application state + param value - APPLY, ENFORCE, or UNAPPLY + + The behavior of the gp_log depends on whether we are applying policy, + enforcing policy, or unapplying policy. During an apply, old settings + are recorded in the log. During an enforce, settings are being applied + but the gp_log does not change. During an unapply, additions to the log + should be ignored (since function calls to apply settings are actually + reverting policy), but removals from the log are allowed. + ''' + # If we're enforcing, but we've unapplied, apply instead + if value == GPOSTATE.ENFORCE: + user_obj = self.gpdb.find('user[@name="%s"]' % self.user) + apply_log = user_obj.find('applylog') + if apply_log is None or len(apply_log) == 0: + self._state = GPOSTATE.APPLY + else: + self._state = value + else: + self._state = value + + def get_state(self): + '''Check the GPOSTATE + ''' + return self._state + + def set_guid(self, guid): + ''' Log to a different GPO guid + param guid - guid value of the GPO from which we're applying + policy + ''' + self.guid = guid + user_obj = self.gpdb.find('user[@name="%s"]' % self.user) + obj = user_obj.find('guid[@value="%s"]' % guid) + if obj is None: + obj = etree.SubElement(user_obj, 'guid') + obj.attrib['value'] = guid + if self._state == GPOSTATE.APPLY: + apply_log = user_obj.find('applylog') + if apply_log is None: + apply_log = etree.SubElement(user_obj, 'applylog') + prev = apply_log.find('guid[@value="%s"]' % guid) + if prev is None: + item = etree.SubElement(apply_log, 'guid') + item.attrib['count'] = '%d' % (len(apply_log) - 1) + item.attrib['value'] = guid + + def store(self, gp_ext_name, attribute, old_val): + ''' Store an attribute in the gp_log + param gp_ext_name - Name of the extension applying policy + param attribute - The attribute being modified + param old_val - The value of the attribute prior to policy + application + ''' + if self._state == GPOSTATE.UNAPPLY or self._state == GPOSTATE.ENFORCE: + return None + user_obj = self.gpdb.find('user[@name="%s"]' % self.user) + guid_obj = user_obj.find('guid[@value="%s"]' % self.guid) + assert guid_obj is not None, "gpo guid was not set" + ext = guid_obj.find('gp_ext[@name="%s"]' % gp_ext_name) + if ext is None: + ext = etree.SubElement(guid_obj, 'gp_ext') + ext.attrib['name'] = gp_ext_name + attr = ext.find('attribute[@name="%s"]' % attribute) + if attr is None: + attr = etree.SubElement(ext, 'attribute') + attr.attrib['name'] = attribute + attr.text = old_val + + def retrieve(self, gp_ext_name, attribute): + ''' Retrieve a stored attribute from the gp_log + param gp_ext_name - Name of the extension which applied policy + param attribute - The attribute being retrieved + return - The value of the attribute prior to policy + application + ''' + user_obj = self.gpdb.find('user[@name="%s"]' % self.user) + guid_obj = user_obj.find('guid[@value="%s"]' % self.guid) + assert guid_obj is not None, "gpo guid was not set" + ext = guid_obj.find('gp_ext[@name="%s"]' % gp_ext_name) + if ext is not None: + attr = ext.find('attribute[@name="%s"]' % attribute) + if attr is not None: + return attr.text + return None + + def retrieve_all(self, gp_ext_name): + ''' Retrieve all stored attributes for this user, GPO guid, and CSE + param gp_ext_name - Name of the extension which applied policy + return - The values of the attributes prior to policy + application + ''' + user_obj = self.gpdb.find('user[@name="%s"]' % self.user) + guid_obj = user_obj.find('guid[@value="%s"]' % self.guid) + assert guid_obj is not None, "gpo guid was not set" + ext = guid_obj.find('gp_ext[@name="%s"]' % gp_ext_name) + if ext is not None: + attrs = ext.findall('attribute') + return {attr.attrib['name']: attr.text for attr in attrs} + return {} + + def get_applied_guids(self): + ''' Return a list of applied ext guids + return - List of guids for gpos that have applied settings + to the system. + ''' + guids = [] + user_obj = self.gpdb.find('user[@name="%s"]' % self.user) + if user_obj is not None: + apply_log = user_obj.find('applylog') + if apply_log is not None: + guid_objs = apply_log.findall('guid[@count]') + guids_by_count = [(g.get('count'), g.get('value')) + for g in guid_objs] + guids_by_count.sort(reverse=True) + guids.extend(guid for count, guid in guids_by_count) + return guids + + def get_applied_settings(self, guids): + ''' Return a list of applied ext guids + return - List of tuples containing the guid of a gpo, then + a dictionary of policies and their values prior + policy application. These are sorted so that the + most recently applied settings are removed first. + ''' + ret = [] + user_obj = self.gpdb.find('user[@name="%s"]' % self.user) + for guid in guids: + guid_settings = user_obj.find('guid[@value="%s"]' % guid) + exts = guid_settings.findall('gp_ext') + settings = {} + for ext in exts: + attr_dict = {} + attrs = ext.findall('attribute') + for attr in attrs: + attr_dict[attr.attrib['name']] = attr.text + settings[ext.attrib['name']] = attr_dict + ret.append((guid, settings)) + return ret + + def delete(self, gp_ext_name, attribute): + ''' Remove an attribute from the gp_log + param gp_ext_name - name of extension from which to remove the + attribute + param attribute - attribute to remove + ''' + user_obj = self.gpdb.find('user[@name="%s"]' % self.user) + guid_obj = user_obj.find('guid[@value="%s"]' % self.guid) + assert guid_obj is not None, "gpo guid was not set" + ext = guid_obj.find('gp_ext[@name="%s"]' % gp_ext_name) + if ext is not None: + attr = ext.find('attribute[@name="%s"]' % attribute) + if attr is not None: + ext.remove(attr) + if len(ext) == 0: + guid_obj.remove(ext) + + def commit(self): + ''' Write gp_log changes to disk ''' + self.gpostore.store(self.username, etree.tostring(self.gpdb, 'utf-8')) + + +class GPOStorage: + def __init__(self, log_file): + if os.path.isfile(log_file): + self.log = tdb.open(log_file) + else: + self.log = tdb.Tdb(log_file, 0, tdb.DEFAULT, os.O_CREAT | os.O_RDWR) + + def start(self): + self.log.transaction_start() + + def get_int(self, key): + try: + return int(self.log.get(get_bytes(key))) + except TypeError: + return None + + def get(self, key): + return self.log.get(get_bytes(key)) + + def get_gplog(self, user): + return gp_log(user, self, self.log.get(get_bytes(user))) + + def store(self, key, val): + self.log.store(get_bytes(key), get_bytes(val)) + + def cancel(self): + self.log.transaction_cancel() + + def delete(self, key): + self.log.delete(get_bytes(key)) + + def commit(self): + self.log.transaction_commit() + + def __del__(self): + self.log.close() + + +class gp_ext(object): + __metaclass__ = ABCMeta + + def __init__(self, lp, creds, username, store): + self.lp = lp + self.creds = creds + self.username = username + self.gp_db = store.get_gplog(username) + + @abstractmethod + def process_group_policy(self, deleted_gpo_list, changed_gpo_list): + pass + + @abstractmethod + def read(self, policy): + pass + + def parse(self, afile): + local_path = self.lp.cache_path('gpo_cache') + data_file = os.path.join(local_path, check_safe_path(afile).upper()) + if os.path.exists(data_file): + return self.read(data_file) + return None + + @abstractmethod + def __str__(self): + pass + + @abstractmethod + def rsop(self, gpo): + return {} + + +class gp_inf_ext(gp_ext): + def read(self, data_file): + policy = open(data_file, 'rb').read() + inf_conf = ConfigParser(interpolation=None) + inf_conf.optionxform = str + try: + inf_conf.readfp(StringIO(policy.decode())) + except UnicodeDecodeError: + inf_conf.readfp(StringIO(policy.decode('utf-16'))) + return inf_conf + + +class gp_pol_ext(gp_ext): + def read(self, data_file): + raw = open(data_file, 'rb').read() + return ndr_unpack(preg.file, raw) + + +class gp_xml_ext(gp_ext): + def read(self, data_file): + raw = open(data_file, 'rb').read() + try: + return etree.fromstring(raw.decode()) + except UnicodeDecodeError: + return etree.fromstring(raw.decode('utf-16')) + + +class gp_applier(object): + '''Group Policy Applier/Unapplier/Modifier + The applier defines functions for monitoring policy application, + removal, and modification. It must be a multi-derived class paired + with a subclass of gp_ext. + ''' + __metaclass__ = ABCMeta + + def cache_add_attribute(self, guid, attribute, value): + '''Add an attribute and value to the Group Policy cache + guid - The GPO guid which applies this policy + attribute - The attribute name of the policy being applied + value - The value of the policy being applied + + Normally called by the subclass apply() function after applying policy. + ''' + self.gp_db.set_guid(guid) + self.gp_db.store(str(self), attribute, value) + self.gp_db.commit() + + def cache_remove_attribute(self, guid, attribute): + '''Remove an attribute from the Group Policy cache + guid - The GPO guid which applies this policy + attribute - The attribute name of the policy being unapplied + + Normally called by the subclass unapply() function when removing old + policy. + ''' + self.gp_db.set_guid(guid) + self.gp_db.delete(str(self), attribute) + self.gp_db.commit() + + def cache_get_attribute_value(self, guid, attribute): + '''Retrieve the value stored in the cache for the given attribute + guid - The GPO guid which applies this policy + attribute - The attribute name of the policy + ''' + self.gp_db.set_guid(guid) + return self.gp_db.retrieve(str(self), attribute) + + def cache_get_all_attribute_values(self, guid): + '''Retrieve all attribute/values currently stored for this gpo+policy + guid - The GPO guid which applies this policy + ''' + self.gp_db.set_guid(guid) + return self.gp_db.retrieve_all(str(self)) + + def cache_get_apply_state(self): + '''Return the current apply state + return - APPLY|ENFORCE|UNAPPLY + ''' + return self.gp_db.get_state() + + def generate_attribute(self, name, *args): + '''Generate an attribute name from arbitrary data + name - A name to ensure uniqueness + args - Any arbitrary set of args, str or bytes + return - A blake2b digest of the data, the attribute + + The importance here is the digest of the data makes the attribute + reproducible and uniquely identifies it. Hashing the name with + the data ensures we don't falsly identify a match which is the same + text in a different file. Using this attribute generator is optional. + ''' + data = b''.join([get_bytes(arg) for arg in [*args]]) + return blake2b(get_bytes(name)+data).hexdigest() + + def generate_value_hash(self, *args): + '''Generate a unique value which identifies value changes + args - Any arbitrary set of args, str or bytes + return - A blake2b digest of the data, the value represented + ''' + data = b''.join([get_bytes(arg) for arg in [*args]]) + return blake2b(data).hexdigest() + + @abstractmethod + def unapply(self, guid, attribute, value): + '''Group Policy Unapply + guid - The GPO guid which applies this policy + attribute - The attribute name of the policy being unapplied + value - The value of the policy being unapplied + ''' + pass + + @abstractmethod + def apply(self, guid, attribute, applier_func, *args): + '''Group Policy Apply + guid - The GPO guid which applies this policy + attribute - The attribute name of the policy being applied + applier_func - An applier function which takes variable args + args - The variable arguments to pass to applier_func + + The applier_func function MUST return the value of the policy being + applied. It's important that implementations of `apply` check for and + first unapply any changed policy. See for example calls to + `cache_get_all_attribute_values()` which searches for all policies + applied by this GPO for this Client Side Extension (CSE). + ''' + pass + + def clean(self, guid, keep=None, remove=None, **kwargs): + '''Cleanup old removed attributes + keep - A list of attributes to keep + remove - A single attribute to remove, or a list of attributes to + remove + kwargs - Additional keyword args required by the subclass unapply + function + + This is only necessary for CSEs which provide multiple attributes. + ''' + # Clean syntax is, either provide a single remove attribute, + # or a list of either removal attributes or keep attributes. + if keep is None: + keep = [] + if remove is None: + remove = [] + + if type(remove) != list: + value = self.cache_get_attribute_value(guid, remove) + if value is not None: + self.unapply(guid, remove, value, **kwargs) + else: + old_vals = self.cache_get_all_attribute_values(guid) + for attribute, value in old_vals.items(): + if (len(remove) > 0 and attribute in remove) or \ + (len(keep) > 0 and attribute not in keep): + self.unapply(guid, attribute, value, **kwargs) + + +class gp_file_applier(gp_applier): + '''Group Policy File Applier/Unapplier/Modifier + Subclass of abstract class gp_applier for monitoring policy applied + via a file. + ''' + + def __generate_value(self, value_hash, files, sep): + data = [value_hash] + data.extend(files) + return sep.join(data) + + def __parse_value(self, value, sep): + '''Parse a value + return - A unique HASH, followed by the file list + ''' + if value is None: + return None, [] + data = value.split(sep) + if '/' in data[0]: + # The first element is not a hash, but a filename. This is a + # legacy value. + return None, data + else: + return data[0], data[1:] if len(data) > 1 else [] + + def unapply(self, guid, attribute, files, sep=':'): + # If the value isn't a list of files, parse value from the log + if type(files) != list: + _, files = self.__parse_value(files, sep) + for file in files: + if os.path.exists(file): + os.unlink(file) + self.cache_remove_attribute(guid, attribute) + + def apply(self, guid, attribute, value_hash, applier_func, *args, sep=':'): + ''' + applier_func MUST return a list of files created by the applier. + + This applier is for policies which only apply to a single file (with + a couple small exceptions). This applier will remove any policy applied + by this GPO which doesn't match the new policy. + ''' + # If the policy has changed, unapply, then apply new policy + old_val = self.cache_get_attribute_value(guid, attribute) + # Ignore removal if this policy is applied and hasn't changed + old_val_hash, old_val_files = self.__parse_value(old_val, sep) + if (old_val_hash != value_hash or \ + self.cache_get_apply_state() == GPOSTATE.ENFORCE) or \ + not all([os.path.exists(f) for f in old_val_files]): + self.unapply(guid, attribute, old_val_files) + else: + # If policy is already applied, skip application + return + + # Apply the policy and log the changes + files = applier_func(*args) + new_value = self.__generate_value(value_hash, files, sep) + self.cache_add_attribute(guid, attribute, new_value) + + +''' Fetch the hostname of a writable DC ''' + + +def get_dc_hostname(creds, lp): + net = Net(creds=creds, lp=lp) + cldap_ret = net.finddc(domain=lp.get('realm'), flags=(nbt.NBT_SERVER_LDAP | + nbt.NBT_SERVER_DS)) + return cldap_ret.pdc_dns_name + + +''' Fetch a list of GUIDs for applicable GPOs ''' + + +def get_gpo_list(dc_hostname, creds, lp, username): + gpos = [] + ads = gpo.ADS_STRUCT(dc_hostname, lp, creds) + if ads.connect(): + # username is DOM\\SAM, but get_gpo_list expects SAM + gpos = ads.get_gpo_list(username.split('\\')[-1]) + return gpos + + +def cache_gpo_dir(conn, cache, sub_dir): + loc_sub_dir = sub_dir.upper() + local_dir = os.path.join(cache, loc_sub_dir) + try: + os.makedirs(local_dir, mode=0o755) + except OSError as e: + if e.errno != errno.EEXIST: + raise + for fdata in conn.list(sub_dir): + if fdata['attrib'] & libsmb.FILE_ATTRIBUTE_DIRECTORY: + cache_gpo_dir(conn, cache, os.path.join(sub_dir, fdata['name'])) + else: + local_name = fdata['name'].upper() + f = NamedTemporaryFile(delete=False, dir=local_dir) + fname = os.path.join(sub_dir, fdata['name']).replace('/', '\\') + f.write(conn.loadfile(fname)) + f.close() + os.rename(f.name, os.path.join(local_dir, local_name)) + + +def check_safe_path(path): + dirs = re.split('/|\\\\', path) + if 'sysvol' in path.lower(): + ldirs = re.split('/|\\\\', path.lower()) + dirs = dirs[ldirs.index('sysvol') + 1:] + if '..' not in dirs: + return os.path.join(*dirs) + raise OSError(path) + + +def check_refresh_gpo_list(dc_hostname, lp, creds, gpos): + # Force signing for the connection + saved_signing_state = creds.get_smb_signing() + creds.set_smb_signing(SMB_SIGNING_REQUIRED) + conn = libsmb.Conn(dc_hostname, 'sysvol', lp=lp, creds=creds) + # Reset signing state + creds.set_smb_signing(saved_signing_state) + cache_path = lp.cache_path('gpo_cache') + for gpo_obj in gpos: + if not gpo_obj.file_sys_path: + continue + cache_gpo_dir(conn, cache_path, check_safe_path(gpo_obj.file_sys_path)) + + +def get_deleted_gpos_list(gp_db, gpos): + applied_gpos = gp_db.get_applied_guids() + current_guids = set([p.name for p in gpos]) + deleted_gpos = [guid for guid in applied_gpos if guid not in current_guids] + return gp_db.get_applied_settings(deleted_gpos) + +def gpo_version(lp, path): + # gpo.gpo_get_sysvol_gpt_version() reads the GPT.INI from a local file, + # read from the gpo client cache. + gpt_path = lp.cache_path(os.path.join('gpo_cache', path)) + return int(gpo.gpo_get_sysvol_gpt_version(gpt_path)[1]) + + +def apply_gp(lp, creds, store, gp_extensions, username, target, force=False): + gp_db = store.get_gplog(username) + dc_hostname = get_dc_hostname(creds, lp) + gpos = get_gpo_list(dc_hostname, creds, lp, username) + del_gpos = get_deleted_gpos_list(gp_db, gpos) + try: + check_refresh_gpo_list(dc_hostname, lp, creds, gpos) + except: + log.error('Failed downloading gpt cache from \'%s\' using SMB' + % dc_hostname) + return + + if force: + changed_gpos = gpos + gp_db.state(GPOSTATE.ENFORCE) + else: + changed_gpos = [] + for gpo_obj in gpos: + if not gpo_obj.file_sys_path: + continue + guid = gpo_obj.name + path = check_safe_path(gpo_obj.file_sys_path).upper() + version = gpo_version(lp, path) + if version != store.get_int(guid): + log.info('GPO %s has changed' % guid) + changed_gpos.append(gpo_obj) + gp_db.state(GPOSTATE.APPLY) + + store.start() + for ext in gp_extensions: + try: + ext = ext(lp, creds, username, store) + if target == 'Computer': + ext.process_group_policy(del_gpos, changed_gpos) + else: + drop_privileges(creds.get_principal(), ext.process_group_policy, + del_gpos, changed_gpos) + except Exception as e: + log.error('Failed to apply extension %s' % str(ext)) + _, _, tb = sys.exc_info() + filename, line_number, _, _ = traceback.extract_tb(tb)[-1] + log.error('%s:%d: %s: %s' % (filename, line_number, + type(e).__name__, str(e))) + continue + for gpo_obj in gpos: + if not gpo_obj.file_sys_path: + continue + guid = gpo_obj.name + path = check_safe_path(gpo_obj.file_sys_path).upper() + version = gpo_version(lp, path) + store.store(guid, '%i' % version) + store.commit() + + +def unapply_gp(lp, creds, store, gp_extensions, username, target): + gp_db = store.get_gplog(username) + gp_db.state(GPOSTATE.UNAPPLY) + # Treat all applied gpos as deleted + del_gpos = gp_db.get_applied_settings(gp_db.get_applied_guids()) + store.start() + for ext in gp_extensions: + try: + ext = ext(lp, creds, username, store) + if target == 'Computer': + ext.process_group_policy(del_gpos, []) + else: + drop_privileges(username, ext.process_group_policy, + del_gpos, []) + except Exception as e: + log.error('Failed to unapply extension %s' % str(ext)) + log.error('Message was: ' + str(e)) + continue + store.commit() + + +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' + '\n'.join(ret) + elif type(vals) == list: + ret = [' '*level + '[ %s ]' % __rsop_vals(v, level+2) for v in vals] + return '\n' + '\n'.join(ret) + else: + if isinstance(vals, numbers.Number): + return ' '*(level+2) + str(vals) + else: + return ' '*(level+2) + get_string(vals) + +def rsop(lp, creds, store, gp_extensions, username, target): + dc_hostname = get_dc_hostname(creds, lp) + gpos = get_gpo_list(dc_hostname, creds, lp, username) + check_refresh_gpo_list(dc_hostname, lp, creds, gpos) + + print('Resultant Set of Policy') + print('%s Policy\n' % target) + term_width = shutil.get_terminal_size(fallback=(120, 50))[0] + for gpo_obj in gpos: + if gpo_obj.display_name.strip() == 'Local Policy': + continue # We never apply local policy + print('GPO: %s' % gpo_obj.display_name) + print('='*term_width) + for ext in gp_extensions: + ext = ext(lp, creds, username, store) + cse_name_m = re.findall(r"'([\w\.]+)'", str(type(ext))) + if len(cse_name_m) > 0: + cse_name = cse_name_m[-1].split('.')[-1] + else: + cse_name = ext.__module__.split('.')[-1] + print(' CSE: %s' % cse_name) + print(' ' + ('-'*int(term_width/2))) + for section, settings in ext.rsop(gpo_obj).items(): + print(' Policy Type: %s' % section) + print(' ' + ('-'*int(term_width/2))) + print(__rsop_vals(settings).lstrip('\n')) + print(' ' + ('-'*int(term_width/2))) + print(' ' + ('-'*int(term_width/2))) + print('%s\n' % ('='*term_width)) + + +def parse_gpext_conf(smb_conf): + from samba.samba3 import param as s3param + lp = s3param.get_context() + if smb_conf is not None: + lp.load(smb_conf) + else: + lp.load_default() + ext_conf = lp.state_path('gpext.conf') + parser = ConfigParser(interpolation=None) + parser.read(ext_conf) + return lp, parser + + +def atomic_write_conf(lp, parser): + ext_conf = lp.state_path('gpext.conf') + with NamedTemporaryFile(mode="w+", delete=False, dir=os.path.dirname(ext_conf)) as f: + parser.write(f) + os.rename(f.name, ext_conf) + + +def check_guid(guid): + # Check for valid guid with curly braces + if guid[0] != '{' or guid[-1] != '}' or len(guid) != 38: + return False + try: + UUID(guid, version=4) + except ValueError: + return False + return True + + +def register_gp_extension(guid, name, path, + smb_conf=None, machine=True, user=True): + # Check that the module exists + if not os.path.exists(path): + return False + if not check_guid(guid): + return False + + lp, parser = parse_gpext_conf(smb_conf) + if guid not in parser.sections(): + parser.add_section(guid) + parser.set(guid, 'DllName', path) + parser.set(guid, 'ProcessGroupPolicy', name) + parser.set(guid, 'NoMachinePolicy', "0" if machine else "1") + parser.set(guid, 'NoUserPolicy', "0" if user else "1") + + atomic_write_conf(lp, parser) + + return True + + +def list_gp_extensions(smb_conf=None): + _, parser = parse_gpext_conf(smb_conf) + results = {} + for guid in parser.sections(): + results[guid] = {} + results[guid]['DllName'] = parser.get(guid, 'DllName') + results[guid]['ProcessGroupPolicy'] = \ + parser.get(guid, 'ProcessGroupPolicy') + results[guid]['MachinePolicy'] = \ + not int(parser.get(guid, 'NoMachinePolicy')) + results[guid]['UserPolicy'] = not int(parser.get(guid, 'NoUserPolicy')) + return results + + +def unregister_gp_extension(guid, smb_conf=None): + if not check_guid(guid): + return False + + lp, parser = parse_gpext_conf(smb_conf) + if guid in parser.sections(): + parser.remove_section(guid) + + atomic_write_conf(lp, parser) + + return True + + +def set_privileges(username, uid, gid): + ''' + Set current process privileges + ''' + + os.setegid(gid) + os.seteuid(uid) + + +def drop_privileges(username, func, *args): + ''' + Run supplied function with privileges for specified username. + ''' + current_uid = os.getuid() + + if not current_uid == 0: + raise Exception('Not enough permissions to drop privileges') + + user_uid = pwd.getpwnam(username).pw_uid + user_gid = pwd.getpwnam(username).pw_gid + + # Drop privileges + set_privileges(username, user_uid, user_gid) + + # We need to catch exception in order to be able to restore + # privileges later in this function + out = None + exc = None + try: + out = func(*args) + except Exception as e: + exc = e + + # Restore privileges + set_privileges('root', current_uid, 0) + + if exc: + raise exc + + return out diff --git a/internal/policies/certificate/python/vendor_samba/gp/util/logging.py b/internal/policies/certificate/python/vendor_samba/gp/util/logging.py new file mode 100644 index 000000000..a74a8707d --- /dev/null +++ b/internal/policies/certificate/python/vendor_samba/gp/util/logging.py @@ -0,0 +1,112 @@ +# +# samba-gpupdate enhanced logging +# +# Copyright (C) 2019-2020 BaseALT Ltd. +# Copyright (C) David Mulder 2022 +# +# 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 . + +import json +import datetime +import logging +import gettext +import random +import sys + +logger = logging.getLogger() +def logger_init(name, log_level): + logger = logging.getLogger(name) + logger.addHandler(logging.StreamHandler(sys.stdout)) + logger.setLevel(logging.CRITICAL) + if log_level == 1: + logger.setLevel(logging.ERROR) + elif log_level == 2: + logger.setLevel(logging.WARNING) + elif log_level == 3: + logger.setLevel(logging.INFO) + elif log_level >= 4: + logger.setLevel(logging.DEBUG) + +class slogm(object): + ''' + Structured log message class + ''' + def __init__(self, message, kwargs=None): + if kwargs is None: + kwargs = {} + self.message = message + self.kwargs = kwargs + if not isinstance(self.kwargs, dict): + self.kwargs = { 'val': self.kwargs } + + def __str__(self): + now = str(datetime.datetime.now().isoformat(sep=' ', timespec='milliseconds')) + args = dict() + args.update(self.kwargs) + result = '{}|{} | {}'.format(now, self.message, args) + + return result + +def message_with_code(mtype, message): + random.seed(message) + code = random.randint(0, 99999) + return '[' + mtype + str(code).rjust(5, '0') + ']| ' + \ + gettext.gettext(message) + +class log(object): + @staticmethod + def info(message, data=None): + if data is None: + data = {} + msg = message_with_code('I', message) + logger.info(slogm(msg, data)) + return msg + + @staticmethod + def warning(message, data=None): + if data is None: + data = {} + msg = message_with_code('W', message) + logger.warning(slogm(msg, data)) + return msg + + @staticmethod + def warn(message, data=None): + if data is None: + data = {} + return log.warning(message, data) + + @staticmethod + def error(message, data=None): + if data is None: + data = {} + msg = message_with_code('E', message) + logger.error(slogm(msg, data)) + return msg + + @staticmethod + def fatal(message, data=None): + if data is None: + data = {} + msg = message_with_code('F', message) + logger.fatal(slogm(msg, data)) + return msg + + @staticmethod + def debug(message, data=None): + if data is None: + data = {} + msg = message_with_code('D', message) + logger.debug(slogm(msg, data)) + return msg diff --git a/internal/policies/certificate/testdata/TestCertAutoenrollScript/golden/enroll_with_empty_advanced_configuration b/internal/policies/certificate/testdata/TestCertAutoenrollScript/golden/enroll_with_empty_advanced_configuration new file mode 100644 index 000000000..a031f00f8 --- /dev/null +++ b/internal/policies/certificate/testdata/TestCertAutoenrollScript/golden/enroll_with_empty_advanced_configuration @@ -0,0 +1,11 @@ +Loading smb.conf +[global] +realm = example.com + +Loading state file: #TMPDIR#/state/cert_gpo_state_keypress.tdb +Enroll called + +guid: adsys-cert-autoenroll-keypress +trust_dir: #TMPDIR#/trust; exists: True +private_dir: #TMPDIR#/private; exists: True +global_trust_dir: #TMPDIR#/ca-certificates; exists: False diff --git a/internal/policies/certificate/testdata/TestCertAutoenrollScript/golden/enroll_with_simple_configuration b/internal/policies/certificate/testdata/TestCertAutoenrollScript/golden/enroll_with_simple_configuration new file mode 100644 index 000000000..a031f00f8 --- /dev/null +++ b/internal/policies/certificate/testdata/TestCertAutoenrollScript/golden/enroll_with_simple_configuration @@ -0,0 +1,11 @@ +Loading smb.conf +[global] +realm = example.com + +Loading state file: #TMPDIR#/state/cert_gpo_state_keypress.tdb +Enroll called + +guid: adsys-cert-autoenroll-keypress +trust_dir: #TMPDIR#/trust; exists: True +private_dir: #TMPDIR#/private; exists: True +global_trust_dir: #TMPDIR#/ca-certificates; exists: False diff --git a/internal/policies/certificate/testdata/TestCertAutoenrollScript/golden/enroll_with_simple_configuration_and_debug_enabled b/internal/policies/certificate/testdata/TestCertAutoenrollScript/golden/enroll_with_simple_configuration_and_debug_enabled new file mode 100644 index 000000000..5e283063c --- /dev/null +++ b/internal/policies/certificate/testdata/TestCertAutoenrollScript/golden/enroll_with_simple_configuration_and_debug_enabled @@ -0,0 +1,12 @@ +Loading smb.conf +[global] +realm = example.com +log level = 10 + +Loading state file: #TMPDIR#/state/cert_gpo_state_keypress.tdb +Enroll called + +guid: adsys-cert-autoenroll-keypress +trust_dir: #TMPDIR#/trust; exists: True +private_dir: #TMPDIR#/private; exists: True +global_trust_dir: #TMPDIR#/ca-certificates; exists: False diff --git a/internal/policies/certificate/testdata/TestCertAutoenrollScript/golden/enroll_with_valid_advanced_configuration b/internal/policies/certificate/testdata/TestCertAutoenrollScript/golden/enroll_with_valid_advanced_configuration new file mode 100644 index 000000000..9fe4ef1bf --- /dev/null +++ b/internal/policies/certificate/testdata/TestCertAutoenrollScript/golden/enroll_with_valid_advanced_configuration @@ -0,0 +1,48 @@ +Loading smb.conf +[global] +realm = example.com + +Loading state file: #TMPDIR#/state/cert_gpo_state_keypress.tdb +Enroll called + +guid: adsys-cert-autoenroll-keypress +trust_dir: #TMPDIR#/trust; exists: True +private_dir: #TMPDIR#/private; exists: True +global_trust_dir: #TMPDIR#/ca-certificates; exists: False + +entries: +keyname: Software\Policies\Microsoft\Cryptography\PolicyServers\37c9dc30f207f27f61a2f7c3aed598a6e2920b54 +valuename: AuthFlags +type: 4 +data: 2 + +keyname: Software\Policies\Microsoft\Cryptography\PolicyServers\37c9dc30f207f27f61a2f7c3aed598a6e2920b54 +valuename: Cost +type: 4 +data: 2147483645 + +keyname: Software\Policies\Microsoft\Cryptography\PolicyServers\37c9dc30f207f27f61a2f7c3aed598a6e2920b54 +valuename: Flags +type: 4 +data: 20 + +keyname: Software\Policies\Microsoft\Cryptography\PolicyServers\37c9dc30f207f27f61a2f7c3aed598a6e2920b54 +valuename: FriendlyName +type: 1 +data: ActiveDirectoryEnrollmentPolicy + +keyname: Software\Policies\Microsoft\Cryptography\PolicyServers\37c9dc30f207f27f61a2f7c3aed598a6e2920b54 +valuename: PolicyID +type: 1 +data: {A5E9BF57-71C6-443A-B7FC-79EFA6F73EBD} + +keyname: Software\Policies\Microsoft\Cryptography\PolicyServers\37c9dc30f207f27f61a2f7c3aed598a6e2920b54 +valuename: URL +type: 1 +data: LDAP: + +keyname: Software\Policies\Microsoft\Cryptography\PolicyServers +valuename: Flags +type: 4 +data: 0 + diff --git a/internal/policies/certificate/testdata/TestCertAutoenrollScript/golden/unenroll b/internal/policies/certificate/testdata/TestCertAutoenrollScript/golden/unenroll new file mode 100644 index 000000000..8b44d2e33 --- /dev/null +++ b/internal/policies/certificate/testdata/TestCertAutoenrollScript/golden/unenroll @@ -0,0 +1,8 @@ +Loading smb.conf +[global] +realm = example.com + +Loading state file: #TMPDIR#/state/cert_gpo_state_keypress.tdb +Unenroll called +guid: adsys-cert-autoenroll-keypress +remove: ['ZXhhbXBsZS1DQQ=='] diff --git a/internal/policies/certificate/testdata/TestPolicyApply/golden/computer,_configured_to_enroll b/internal/policies/certificate/testdata/TestPolicyApply/golden/computer,_configured_to_enroll new file mode 100644 index 000000000..b53aaaf88 --- /dev/null +++ b/internal/policies/certificate/testdata/TestPolicyApply/golden/computer,_configured_to_enroll @@ -0,0 +1,3 @@ +enroll keypress example.com --policy_servers_json null --state_dir #TMPDIR#/statedir +KRB5CCNAME=#TMPDIR#/rundir/krb5cc/keypress +PYTHONPATH=#TMPDIR#/sharedir/python diff --git a/internal/policies/certificate/testdata/TestPolicyApply/golden/computer,_configured_to_enroll,_advanced_configuration b/internal/policies/certificate/testdata/TestPolicyApply/golden/computer,_configured_to_enroll,_advanced_configuration new file mode 100644 index 000000000..ebedab266 --- /dev/null +++ b/internal/policies/certificate/testdata/TestPolicyApply/golden/computer,_configured_to_enroll,_advanced_configuration @@ -0,0 +1,3 @@ +enroll keypress example.com --policy_servers_json [{"keyname":"Software\\Policies\\Microsoft\\Cryptography\\PolicyServers\\37c9dc30f207f27f61a2f7c3aed598a6e2920b54","valuename":"AuthFlags","data":2,"type":4},{"keyname":"Software\\Policies\\Microsoft\\Cryptography\\PolicyServers\\37c9dc30f207f27f61a2f7c3aed598a6e2920b54","valuename":"Cost","data":2147483645,"type":4},{"keyname":"Software\\Policies\\Microsoft\\Cryptography\\PolicyServers\\37c9dc30f207f27f61a2f7c3aed598a6e2920b54","valuename":"Flags","data":20,"type":4},{"keyname":"Software\\Policies\\Microsoft\\Cryptography\\PolicyServers\\37c9dc30f207f27f61a2f7c3aed598a6e2920b54","valuename":"FriendlyName","data":"ActiveDirectoryEnrollmentPolicy","type":1},{"keyname":"Software\\Policies\\Microsoft\\Cryptography\\PolicyServers\\37c9dc30f207f27f61a2f7c3aed598a6e2920b54","valuename":"PolicyID","data":"{A5E9BF57-71C6-443A-B7FC-79EFA6F73EBD}","type":1},{"keyname":"Software\\Policies\\Microsoft\\Cryptography\\PolicyServers\\37c9dc30f207f27f61a2f7c3aed598a6e2920b54","valuename":"URL","data":"LDAP:","type":1},{"keyname":"Software\\Policies\\Microsoft\\Cryptography\\PolicyServers","valuename":"Flags","data":0,"type":4}] --state_dir #TMPDIR#/statedir +KRB5CCNAME=#TMPDIR#/rundir/krb5cc/keypress +PYTHONPATH=#TMPDIR#/sharedir/python diff --git a/internal/policies/certificate/testdata/TestPolicyApply/golden/computer,_configured_to_unenroll b/internal/policies/certificate/testdata/TestPolicyApply/golden/computer,_configured_to_unenroll new file mode 100644 index 000000000..87bdd0251 --- /dev/null +++ b/internal/policies/certificate/testdata/TestPolicyApply/golden/computer,_configured_to_unenroll @@ -0,0 +1,3 @@ +unenroll keypress example.com --policy_servers_json null --state_dir #TMPDIR#/statedir +KRB5CCNAME=#TMPDIR#/rundir/krb5cc/keypress +PYTHONPATH=#TMPDIR#/sharedir/python diff --git a/internal/testutils/admock/samba/credentials/__init__.py b/internal/testutils/admock/samba/credentials/__init__.py index 6266c7dcc..2029abf3f 100644 --- a/internal/testutils/admock/samba/credentials/__init__.py +++ b/internal/testutils/admock/samba/credentials/__init__.py @@ -9,3 +9,6 @@ def set_kerberos_state(self, val): def guess(self, val): pass + + def get_username(self): + pass diff --git a/internal/testutils/admock/samba/dcerpc/preg.py b/internal/testutils/admock/samba/dcerpc/preg.py new file mode 100644 index 000000000..6c895c813 --- /dev/null +++ b/internal/testutils/admock/samba/dcerpc/preg.py @@ -0,0 +1,9 @@ +from dataclasses import dataclass +from typing import Any + +@dataclass +class entry: + keyname: str = None + valuename: str = None + type: int = None + data: Any = None diff --git a/internal/testutils/admock/samba/param/__init__.py b/internal/testutils/admock/samba/param/__init__.py index 574eddfc1..f85b80128 100644 --- a/internal/testutils/admock/samba/param/__init__.py +++ b/internal/testutils/admock/samba/param/__init__.py @@ -1,2 +1,6 @@ -def LoadParm(): - return \ No newline at end of file +def LoadParm(smb_conf=None): + if smb_conf is None: + return + print('Loading smb.conf') + with open(smb_conf, 'r') as f: + print(f.read()) diff --git a/internal/testutils/admock/vendor_samba/__init__.py b/internal/testutils/admock/vendor_samba/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/internal/testutils/admock/vendor_samba/gp/__init__.py b/internal/testutils/admock/vendor_samba/gp/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/internal/testutils/admock/vendor_samba/gp/gp_cert_auto_enroll_ext.py b/internal/testutils/admock/vendor_samba/gp/gp_cert_auto_enroll_ext.py new file mode 100644 index 000000000..31f2dbcaa --- /dev/null +++ b/internal/testutils/admock/vendor_samba/gp/gp_cert_auto_enroll_ext.py @@ -0,0 +1,38 @@ +import os + +class gp_cert_auto_enroll_ext(object): + def __init__(self, _lp, _credentials, _username, _store): + pass + + def cache_get_all_attribute_values(self, _guid): + return {'ZXhhbXBsZS1DQQ==': '{"files": ["/var/lib/adsys/certs/galacticcafe-CA.0.crt"]}'} + + def __enroll(self, guid, entries, trust_dir, private_dir, global_trust_dir): + if os.getenv('ADSYS_WANT_AUTOENROLL_ERROR'): + raise Exception('Autoenroll error requested') + + print('Enroll called') + print() + print(f'guid: {guid}') + print(f'trust_dir: {trust_dir}; exists: {os.path.exists(trust_dir)}') + print(f'private_dir: {private_dir}; exists: {os.path.exists(private_dir)}') + print(f'global_trust_dir: {global_trust_dir}; exists: {os.path.exists(global_trust_dir)}') + + if entries == []: + return + + print('\nentries:') + for entry in entries: + print(f'''keyname: {entry.keyname} +valuename: {entry.valuename} +type: {entry.type} +data: {entry.data} +''') + + def clean(self, guid, remove=None): + if os.getenv('ADSYS_WANT_AUTOENROLL_ERROR'): + raise Exception('Autoenroll error requested') + + print('Unenroll called') + print(f'guid: {guid}') + print(f'remove: {remove}') diff --git a/internal/testutils/admock/vendor_samba/gp/gpclass.py b/internal/testutils/admock/vendor_samba/gp/gpclass.py new file mode 100644 index 000000000..0a1735aec --- /dev/null +++ b/internal/testutils/admock/vendor_samba/gp/gpclass.py @@ -0,0 +1,4 @@ +import os + +def GPOStorage(state_file): + print(f'Loading state file: {state_file}') From 4ff12e1e7c0923561e9e515bd289b47545a728d0 Mon Sep 17 00:00:00 2001 From: Gabriel Nagy Date: Wed, 26 Jul 2023 02:26:01 +0300 Subject: [PATCH 04/13] Hook up certificate policy manager to parent manager --- internal/policies/manager.go | 33 +++++++-- internal/policies/manager_test.go | 2 + .../adsys/machine/nested/usr.bin.baz | 1 + .../etc/apparmor.d/adsys/machine/usr.bin.bar | 1 + .../etc/apparmor.d/adsys/machine/usr.bin.foo | 1 + .../etc/dconf/db/machine.d/adsys | 5 ++ .../etc/dconf/db/machine.d/locks/adsys | 2 + .../99-adsys-privilege-enforcement.conf | 6 ++ .../sudoers.d/99-adsys-privilege-enforcement | 9 +++ .../adsys-cifs-example.com-smb_share.mount | 17 +++++ .../adsys-fuse-example.com-ftp_share.mount | 17 +++++ .../adsys-nfs-example.com-nfs_share.mount | 17 +++++ .../run/adsys/machine/scripts/.ready | 0 .../run/adsys/machine/scripts/.running | 0 .../run/adsys/machine/scripts/logoff | 1 + .../run/adsys/machine/scripts/logon | 1 + .../scripts/scripts/empty-subfolder/.empty | 0 .../scripts/scripts/final-machine-script.sh | 1 + .../scripts/otherfolder/script-user-logoff | 1 + .../scripts/scripts/script-machine-shutdown | 1 + .../scripts/scripts/script-machine-startup | 1 + .../machine/scripts/scripts/script-user-logon | 1 + .../scripts/scripts/subfolder/other-script | 1 + .../machine/scripts/scripts/unreferenced-data | 1 + .../scripts/scripts/unreferenced-script | 1 + .../run/adsys/machine/scripts/shutdown | 1 + .../run/adsys/machine/scripts/startup | 3 + .../run/adsys/users/.empty | 0 .../sys/kernel/security/apparmor/profiles | 1 + .../cache/adsys/policies/hostname/assets.db | Bin 0 -> 4485 bytes .../cache/adsys/policies/hostname/policies | 70 ++++++++++++++++++ 31 files changed, 189 insertions(+), 7 deletions(-) create mode 100644 internal/policies/testdata/TestApplyPolicies/golden/succeed_if_checking_for_backend_online_status_returns_an_error/etc/apparmor.d/adsys/machine/nested/usr.bin.baz create mode 100644 internal/policies/testdata/TestApplyPolicies/golden/succeed_if_checking_for_backend_online_status_returns_an_error/etc/apparmor.d/adsys/machine/usr.bin.bar create mode 100644 internal/policies/testdata/TestApplyPolicies/golden/succeed_if_checking_for_backend_online_status_returns_an_error/etc/apparmor.d/adsys/machine/usr.bin.foo create mode 100644 internal/policies/testdata/TestApplyPolicies/golden/succeed_if_checking_for_backend_online_status_returns_an_error/etc/dconf/db/machine.d/adsys create mode 100644 internal/policies/testdata/TestApplyPolicies/golden/succeed_if_checking_for_backend_online_status_returns_an_error/etc/dconf/db/machine.d/locks/adsys create mode 100644 internal/policies/testdata/TestApplyPolicies/golden/succeed_if_checking_for_backend_online_status_returns_an_error/etc/polkit-1/localauthority.conf.d/99-adsys-privilege-enforcement.conf create mode 100644 internal/policies/testdata/TestApplyPolicies/golden/succeed_if_checking_for_backend_online_status_returns_an_error/etc/sudoers.d/99-adsys-privilege-enforcement create mode 100644 internal/policies/testdata/TestApplyPolicies/golden/succeed_if_checking_for_backend_online_status_returns_an_error/etc/systemd/system/adsys-cifs-example.com-smb_share.mount create mode 100644 internal/policies/testdata/TestApplyPolicies/golden/succeed_if_checking_for_backend_online_status_returns_an_error/etc/systemd/system/adsys-fuse-example.com-ftp_share.mount create mode 100644 internal/policies/testdata/TestApplyPolicies/golden/succeed_if_checking_for_backend_online_status_returns_an_error/etc/systemd/system/adsys-nfs-example.com-nfs_share.mount create mode 100644 internal/policies/testdata/TestApplyPolicies/golden/succeed_if_checking_for_backend_online_status_returns_an_error/run/adsys/machine/scripts/.ready create mode 100644 internal/policies/testdata/TestApplyPolicies/golden/succeed_if_checking_for_backend_online_status_returns_an_error/run/adsys/machine/scripts/.running create mode 100644 internal/policies/testdata/TestApplyPolicies/golden/succeed_if_checking_for_backend_online_status_returns_an_error/run/adsys/machine/scripts/logoff create mode 100644 internal/policies/testdata/TestApplyPolicies/golden/succeed_if_checking_for_backend_online_status_returns_an_error/run/adsys/machine/scripts/logon create mode 100644 internal/policies/testdata/TestApplyPolicies/golden/succeed_if_checking_for_backend_online_status_returns_an_error/run/adsys/machine/scripts/scripts/empty-subfolder/.empty create mode 100755 internal/policies/testdata/TestApplyPolicies/golden/succeed_if_checking_for_backend_online_status_returns_an_error/run/adsys/machine/scripts/scripts/final-machine-script.sh create mode 100755 internal/policies/testdata/TestApplyPolicies/golden/succeed_if_checking_for_backend_online_status_returns_an_error/run/adsys/machine/scripts/scripts/otherfolder/script-user-logoff create mode 100755 internal/policies/testdata/TestApplyPolicies/golden/succeed_if_checking_for_backend_online_status_returns_an_error/run/adsys/machine/scripts/scripts/script-machine-shutdown create mode 100755 internal/policies/testdata/TestApplyPolicies/golden/succeed_if_checking_for_backend_online_status_returns_an_error/run/adsys/machine/scripts/scripts/script-machine-startup create mode 100755 internal/policies/testdata/TestApplyPolicies/golden/succeed_if_checking_for_backend_online_status_returns_an_error/run/adsys/machine/scripts/scripts/script-user-logon create mode 100755 internal/policies/testdata/TestApplyPolicies/golden/succeed_if_checking_for_backend_online_status_returns_an_error/run/adsys/machine/scripts/scripts/subfolder/other-script create mode 100644 internal/policies/testdata/TestApplyPolicies/golden/succeed_if_checking_for_backend_online_status_returns_an_error/run/adsys/machine/scripts/scripts/unreferenced-data create mode 100644 internal/policies/testdata/TestApplyPolicies/golden/succeed_if_checking_for_backend_online_status_returns_an_error/run/adsys/machine/scripts/scripts/unreferenced-script create mode 100644 internal/policies/testdata/TestApplyPolicies/golden/succeed_if_checking_for_backend_online_status_returns_an_error/run/adsys/machine/scripts/shutdown create mode 100644 internal/policies/testdata/TestApplyPolicies/golden/succeed_if_checking_for_backend_online_status_returns_an_error/run/adsys/machine/scripts/startup create mode 100644 internal/policies/testdata/TestApplyPolicies/golden/succeed_if_checking_for_backend_online_status_returns_an_error/run/adsys/users/.empty create mode 100644 internal/policies/testdata/TestApplyPolicies/golden/succeed_if_checking_for_backend_online_status_returns_an_error/sys/kernel/security/apparmor/profiles create mode 100644 internal/policies/testdata/TestApplyPolicies/golden/succeed_if_checking_for_backend_online_status_returns_an_error/var/cache/adsys/policies/hostname/assets.db create mode 100644 internal/policies/testdata/TestApplyPolicies/golden/succeed_if_checking_for_backend_online_status_returns_an_error/var/cache/adsys/policies/hostname/policies diff --git a/internal/policies/manager.go b/internal/policies/manager.go index 6a94c4a93..7f1227374 100644 --- a/internal/policies/manager.go +++ b/internal/policies/manager.go @@ -38,6 +38,7 @@ import ( log "github.com/ubuntu/adsys/internal/grpc/logstreamer" "github.com/ubuntu/adsys/internal/i18n" "github.com/ubuntu/adsys/internal/policies/apparmor" + "github.com/ubuntu/adsys/internal/policies/certificate" "github.com/ubuntu/adsys/internal/policies/dconf" "github.com/ubuntu/adsys/internal/policies/entry" "github.com/ubuntu/adsys/internal/policies/gdm" @@ -60,15 +61,16 @@ type Manager struct { policiesCacheDir string hostname string - dconf *dconf.Manager - privilege *privilege.Manager - scripts *scripts.Manager - mount *mount.Manager - gdm *gdm.Manager - apparmor *apparmor.Manager - proxy *proxy.Manager backend backends.Backend + dconf *dconf.Manager + privilege *privilege.Manager + scripts *scripts.Manager + mount *mount.Manager + gdm *gdm.Manager + apparmor *apparmor.Manager + proxy *proxy.Manager + certificate *certificate.Manager subscriptionDbus dbus.BusObject @@ -91,10 +93,12 @@ type systemdCaller interface { type options struct { cacheDir string + stateDir string dconfDir string sudoersDir string policyKitDir string runDir string + shareDir string apparmorDir string apparmorFsDir string systemUnitDir string @@ -209,7 +213,9 @@ func NewManager(bus *dbus.Conn, hostname string, backend backends.Backend, opts // defaults args := options{ cacheDir: consts.DefaultCacheDir, + stateDir: consts.DefaultStateDir, runDir: consts.DefaultRunDir, + shareDir: consts.DefaultShareDir, apparmorDir: consts.DefaultApparmorDir, systemUnitDir: consts.DefaultSystemUnitDir, systemdCaller: defaultSystemdCaller, @@ -259,6 +265,13 @@ func NewManager(bus *dbus.Conn, hostname string, backend backends.Backend, opts } proxyManager := proxy.New(bus, proxyOptions...) + // certificate manager + certificateManager := certificate.New(backend.Domain(), + certificate.WithStateDir(args.stateDir), + certificate.WithRunDir(args.runDir), + certificate.WithShareDir(args.shareDir), + ) + // inject applied dconf mangager if we need to build a gdm manager if args.gdm == nil { if args.gdm, err = gdm.New(gdm.WithDconf(dconfManager)); err != nil { @@ -284,6 +297,7 @@ func NewManager(bus *dbus.Conn, hostname string, backend backends.Backend, opts mount: mountManager, apparmor: apparmorManager, proxy: proxyManager, + certificate: certificateManager, gdm: args.gdm, subscriptionDbus: subscriptionDbus, @@ -341,6 +355,11 @@ func (m *Manager) ApplyPolicies(ctx context.Context, objectName string, isComput g.Go(func() error { return m.proxy.ApplyPolicy(ctx, objectName, isComputer, rules["proxy"]) }) + g.Go(func() error { + // Ignore error as we don't want to fail because of online status this late in the process + isOnline, _ := m.backend.IsOnline() + return m.certificate.ApplyPolicy(ctx, objectName, isComputer, isOnline, rules["certificate"]) + }) if err := g.Wait(); err != nil { return err } diff --git a/internal/policies/manager_test.go b/internal/policies/manager_test.go index 67f70c3af..ebd8a323e 100644 --- a/internal/policies/manager_test.go +++ b/internal/policies/manager_test.go @@ -41,10 +41,12 @@ func TestApplyPolicies(t *testing.T) { isNotSubscribed bool secondCallWithNoSubscription bool noUbuntuProxyManager bool + backendOfflineError bool wantErr bool }{ "Succeed": {policiesDir: "all_entry_types"}, + "Succeed if checking for backend online status returns an error": {backendOfflineError: true, policiesDir: "all_entry_types"}, "Second call with no rules deletes everything": {policiesDir: "all_entry_types", secondCallWithNoRules: true, scriptSessionEndedForSecondCall: true}, "Second call with no rules don't remove scripts if session hasn’t ended": {policiesDir: "all_entry_types", secondCallWithNoRules: true, scriptSessionEndedForSecondCall: false}, diff --git a/internal/policies/testdata/TestApplyPolicies/golden/succeed_if_checking_for_backend_online_status_returns_an_error/etc/apparmor.d/adsys/machine/nested/usr.bin.baz b/internal/policies/testdata/TestApplyPolicies/golden/succeed_if_checking_for_backend_online_status_returns_an_error/etc/apparmor.d/adsys/machine/nested/usr.bin.baz new file mode 100644 index 000000000..c3fdc981e --- /dev/null +++ b/internal/policies/testdata/TestApplyPolicies/golden/succeed_if_checking_for_backend_online_status_returns_an_error/etc/apparmor.d/adsys/machine/nested/usr.bin.baz @@ -0,0 +1 @@ +/usr/bin/baz {} diff --git a/internal/policies/testdata/TestApplyPolicies/golden/succeed_if_checking_for_backend_online_status_returns_an_error/etc/apparmor.d/adsys/machine/usr.bin.bar b/internal/policies/testdata/TestApplyPolicies/golden/succeed_if_checking_for_backend_online_status_returns_an_error/etc/apparmor.d/adsys/machine/usr.bin.bar new file mode 100644 index 000000000..9fc2774f1 --- /dev/null +++ b/internal/policies/testdata/TestApplyPolicies/golden/succeed_if_checking_for_backend_online_status_returns_an_error/etc/apparmor.d/adsys/machine/usr.bin.bar @@ -0,0 +1 @@ +/usr/bin/bar {} diff --git a/internal/policies/testdata/TestApplyPolicies/golden/succeed_if_checking_for_backend_online_status_returns_an_error/etc/apparmor.d/adsys/machine/usr.bin.foo b/internal/policies/testdata/TestApplyPolicies/golden/succeed_if_checking_for_backend_online_status_returns_an_error/etc/apparmor.d/adsys/machine/usr.bin.foo new file mode 100644 index 000000000..450648222 --- /dev/null +++ b/internal/policies/testdata/TestApplyPolicies/golden/succeed_if_checking_for_backend_online_status_returns_an_error/etc/apparmor.d/adsys/machine/usr.bin.foo @@ -0,0 +1 @@ +/usr/bin/foo {} diff --git a/internal/policies/testdata/TestApplyPolicies/golden/succeed_if_checking_for_backend_online_status_returns_an_error/etc/dconf/db/machine.d/adsys b/internal/policies/testdata/TestApplyPolicies/golden/succeed_if_checking_for_backend_online_status_returns_an_error/etc/dconf/db/machine.d/adsys new file mode 100644 index 000000000..3067d2ff6 --- /dev/null +++ b/internal/policies/testdata/TestApplyPolicies/golden/succeed_if_checking_for_backend_online_status_returns_an_error/etc/dconf/db/machine.d/adsys @@ -0,0 +1,5 @@ +[path/to] +key1='ValueOfKey1' +key2='ValueOfKey2 +On +Multilines' diff --git a/internal/policies/testdata/TestApplyPolicies/golden/succeed_if_checking_for_backend_online_status_returns_an_error/etc/dconf/db/machine.d/locks/adsys b/internal/policies/testdata/TestApplyPolicies/golden/succeed_if_checking_for_backend_online_status_returns_an_error/etc/dconf/db/machine.d/locks/adsys new file mode 100644 index 000000000..82ce2888c --- /dev/null +++ b/internal/policies/testdata/TestApplyPolicies/golden/succeed_if_checking_for_backend_online_status_returns_an_error/etc/dconf/db/machine.d/locks/adsys @@ -0,0 +1,2 @@ +/path/to/key1 +/path/to/key2 diff --git a/internal/policies/testdata/TestApplyPolicies/golden/succeed_if_checking_for_backend_online_status_returns_an_error/etc/polkit-1/localauthority.conf.d/99-adsys-privilege-enforcement.conf b/internal/policies/testdata/TestApplyPolicies/golden/succeed_if_checking_for_backend_online_status_returns_an_error/etc/polkit-1/localauthority.conf.d/99-adsys-privilege-enforcement.conf new file mode 100644 index 000000000..dbf1b12ee --- /dev/null +++ b/internal/policies/testdata/TestApplyPolicies/golden/succeed_if_checking_for_backend_online_status_returns_an_error/etc/polkit-1/localauthority.conf.d/99-adsys-privilege-enforcement.conf @@ -0,0 +1,6 @@ +# This file is managed by adsys. +# Do not edit this file manually. +# Any changes will be overwritten. + +[Configuration] +AdminIdentities=unix-user:alice@domain;unix-user:bob@domain2;unix-group:mygroup@domain;unix-user:cosmic carole@domain diff --git a/internal/policies/testdata/TestApplyPolicies/golden/succeed_if_checking_for_backend_online_status_returns_an_error/etc/sudoers.d/99-adsys-privilege-enforcement b/internal/policies/testdata/TestApplyPolicies/golden/succeed_if_checking_for_backend_online_status_returns_an_error/etc/sudoers.d/99-adsys-privilege-enforcement new file mode 100644 index 000000000..e97388f49 --- /dev/null +++ b/internal/policies/testdata/TestApplyPolicies/golden/succeed_if_checking_for_backend_online_status_returns_an_error/etc/sudoers.d/99-adsys-privilege-enforcement @@ -0,0 +1,9 @@ +# This file is managed by adsys. +# Do not edit this file manually. +# Any changes will be overwritten. + +"alice@domain" ALL=(ALL:ALL) ALL +"bob@domain2" ALL=(ALL:ALL) ALL +"%mygroup@domain" ALL=(ALL:ALL) ALL +"cosmic carole@domain" ALL=(ALL:ALL) ALL + diff --git a/internal/policies/testdata/TestApplyPolicies/golden/succeed_if_checking_for_backend_online_status_returns_an_error/etc/systemd/system/adsys-cifs-example.com-smb_share.mount b/internal/policies/testdata/TestApplyPolicies/golden/succeed_if_checking_for_backend_online_status_returns_an_error/etc/systemd/system/adsys-cifs-example.com-smb_share.mount new file mode 100644 index 000000000..74da4d221 --- /dev/null +++ b/internal/policies/testdata/TestApplyPolicies/golden/succeed_if_checking_for_backend_online_status_returns_an_error/etc/systemd/system/adsys-cifs-example.com-smb_share.mount @@ -0,0 +1,17 @@ +# This template defines the basic structure of a mount unit generated by ADSys for system mounts. +[Unit] +Description=ADSys mount for smb://example.com/smb_share +After=network-online.target +Requires=network-online.target + +[Mount] +What=//example.com/smb_share +Where=/adsys/cifs/example.com/smb_share +Type=cifs +Options=defaults +# This option prevents hangs on shutdown due to an unreachable network share. +LazyUnmount=true +TimeoutSec=30 + +[Install] +WantedBy=default.target diff --git a/internal/policies/testdata/TestApplyPolicies/golden/succeed_if_checking_for_backend_online_status_returns_an_error/etc/systemd/system/adsys-fuse-example.com-ftp_share.mount b/internal/policies/testdata/TestApplyPolicies/golden/succeed_if_checking_for_backend_online_status_returns_an_error/etc/systemd/system/adsys-fuse-example.com-ftp_share.mount new file mode 100644 index 000000000..f051513af --- /dev/null +++ b/internal/policies/testdata/TestApplyPolicies/golden/succeed_if_checking_for_backend_online_status_returns_an_error/etc/systemd/system/adsys-fuse-example.com-ftp_share.mount @@ -0,0 +1,17 @@ +# This template defines the basic structure of a mount unit generated by ADSys for system mounts. +[Unit] +Description=ADSys mount for ftp://example.com/ftp_share +After=network-online.target +Requires=network-online.target + +[Mount] +What=curlftpfs#example.com +Where=/adsys/fuse/example.com/ftp_share +Type=fuse +Options=defaults +# This option prevents hangs on shutdown due to an unreachable network share. +LazyUnmount=true +TimeoutSec=30 + +[Install] +WantedBy=default.target diff --git a/internal/policies/testdata/TestApplyPolicies/golden/succeed_if_checking_for_backend_online_status_returns_an_error/etc/systemd/system/adsys-nfs-example.com-nfs_share.mount b/internal/policies/testdata/TestApplyPolicies/golden/succeed_if_checking_for_backend_online_status_returns_an_error/etc/systemd/system/adsys-nfs-example.com-nfs_share.mount new file mode 100644 index 000000000..bdfa1c268 --- /dev/null +++ b/internal/policies/testdata/TestApplyPolicies/golden/succeed_if_checking_for_backend_online_status_returns_an_error/etc/systemd/system/adsys-nfs-example.com-nfs_share.mount @@ -0,0 +1,17 @@ +# This template defines the basic structure of a mount unit generated by ADSys for system mounts. +[Unit] +Description=ADSys mount for nfs://example.com/nfs_share +After=network-online.target +Requires=network-online.target + +[Mount] +What=example.com:/nfs_share +Where=/adsys/nfs/example.com/nfs_share +Type=nfs +Options=defaults +# This option prevents hangs on shutdown due to an unreachable network share. +LazyUnmount=true +TimeoutSec=30 + +[Install] +WantedBy=default.target diff --git a/internal/policies/testdata/TestApplyPolicies/golden/succeed_if_checking_for_backend_online_status_returns_an_error/run/adsys/machine/scripts/.ready b/internal/policies/testdata/TestApplyPolicies/golden/succeed_if_checking_for_backend_online_status_returns_an_error/run/adsys/machine/scripts/.ready new file mode 100644 index 000000000..e69de29bb diff --git a/internal/policies/testdata/TestApplyPolicies/golden/succeed_if_checking_for_backend_online_status_returns_an_error/run/adsys/machine/scripts/.running b/internal/policies/testdata/TestApplyPolicies/golden/succeed_if_checking_for_backend_online_status_returns_an_error/run/adsys/machine/scripts/.running new file mode 100644 index 000000000..e69de29bb diff --git a/internal/policies/testdata/TestApplyPolicies/golden/succeed_if_checking_for_backend_online_status_returns_an_error/run/adsys/machine/scripts/logoff b/internal/policies/testdata/TestApplyPolicies/golden/succeed_if_checking_for_backend_online_status_returns_an_error/run/adsys/machine/scripts/logoff new file mode 100644 index 000000000..f1f55fa88 --- /dev/null +++ b/internal/policies/testdata/TestApplyPolicies/golden/succeed_if_checking_for_backend_online_status_returns_an_error/run/adsys/machine/scripts/logoff @@ -0,0 +1 @@ +scripts/otherfolder/script-user-logoff diff --git a/internal/policies/testdata/TestApplyPolicies/golden/succeed_if_checking_for_backend_online_status_returns_an_error/run/adsys/machine/scripts/logon b/internal/policies/testdata/TestApplyPolicies/golden/succeed_if_checking_for_backend_online_status_returns_an_error/run/adsys/machine/scripts/logon new file mode 100644 index 000000000..0aa0488d4 --- /dev/null +++ b/internal/policies/testdata/TestApplyPolicies/golden/succeed_if_checking_for_backend_online_status_returns_an_error/run/adsys/machine/scripts/logon @@ -0,0 +1 @@ +scripts/script-user-logon diff --git a/internal/policies/testdata/TestApplyPolicies/golden/succeed_if_checking_for_backend_online_status_returns_an_error/run/adsys/machine/scripts/scripts/empty-subfolder/.empty b/internal/policies/testdata/TestApplyPolicies/golden/succeed_if_checking_for_backend_online_status_returns_an_error/run/adsys/machine/scripts/scripts/empty-subfolder/.empty new file mode 100644 index 000000000..e69de29bb diff --git a/internal/policies/testdata/TestApplyPolicies/golden/succeed_if_checking_for_backend_online_status_returns_an_error/run/adsys/machine/scripts/scripts/final-machine-script.sh b/internal/policies/testdata/TestApplyPolicies/golden/succeed_if_checking_for_backend_online_status_returns_an_error/run/adsys/machine/scripts/scripts/final-machine-script.sh new file mode 100755 index 000000000..ae7b4be6c --- /dev/null +++ b/internal/policies/testdata/TestApplyPolicies/golden/succeed_if_checking_for_backend_online_status_returns_an_error/run/adsys/machine/scripts/scripts/final-machine-script.sh @@ -0,0 +1 @@ +final machine script diff --git a/internal/policies/testdata/TestApplyPolicies/golden/succeed_if_checking_for_backend_online_status_returns_an_error/run/adsys/machine/scripts/scripts/otherfolder/script-user-logoff b/internal/policies/testdata/TestApplyPolicies/golden/succeed_if_checking_for_backend_online_status_returns_an_error/run/adsys/machine/scripts/scripts/otherfolder/script-user-logoff new file mode 100755 index 000000000..4080816a8 --- /dev/null +++ b/internal/policies/testdata/TestApplyPolicies/golden/succeed_if_checking_for_backend_online_status_returns_an_error/run/adsys/machine/scripts/scripts/otherfolder/script-user-logoff @@ -0,0 +1 @@ +script user logoff diff --git a/internal/policies/testdata/TestApplyPolicies/golden/succeed_if_checking_for_backend_online_status_returns_an_error/run/adsys/machine/scripts/scripts/script-machine-shutdown b/internal/policies/testdata/TestApplyPolicies/golden/succeed_if_checking_for_backend_online_status_returns_an_error/run/adsys/machine/scripts/scripts/script-machine-shutdown new file mode 100755 index 000000000..4dc4c0713 --- /dev/null +++ b/internal/policies/testdata/TestApplyPolicies/golden/succeed_if_checking_for_backend_online_status_returns_an_error/run/adsys/machine/scripts/scripts/script-machine-shutdown @@ -0,0 +1 @@ +script machine shutdown diff --git a/internal/policies/testdata/TestApplyPolicies/golden/succeed_if_checking_for_backend_online_status_returns_an_error/run/adsys/machine/scripts/scripts/script-machine-startup b/internal/policies/testdata/TestApplyPolicies/golden/succeed_if_checking_for_backend_online_status_returns_an_error/run/adsys/machine/scripts/scripts/script-machine-startup new file mode 100755 index 000000000..5adba498f --- /dev/null +++ b/internal/policies/testdata/TestApplyPolicies/golden/succeed_if_checking_for_backend_online_status_returns_an_error/run/adsys/machine/scripts/scripts/script-machine-startup @@ -0,0 +1 @@ +script machine startup diff --git a/internal/policies/testdata/TestApplyPolicies/golden/succeed_if_checking_for_backend_online_status_returns_an_error/run/adsys/machine/scripts/scripts/script-user-logon b/internal/policies/testdata/TestApplyPolicies/golden/succeed_if_checking_for_backend_online_status_returns_an_error/run/adsys/machine/scripts/scripts/script-user-logon new file mode 100755 index 000000000..e5a483136 --- /dev/null +++ b/internal/policies/testdata/TestApplyPolicies/golden/succeed_if_checking_for_backend_online_status_returns_an_error/run/adsys/machine/scripts/scripts/script-user-logon @@ -0,0 +1 @@ +script user logon diff --git a/internal/policies/testdata/TestApplyPolicies/golden/succeed_if_checking_for_backend_online_status_returns_an_error/run/adsys/machine/scripts/scripts/subfolder/other-script b/internal/policies/testdata/TestApplyPolicies/golden/succeed_if_checking_for_backend_online_status_returns_an_error/run/adsys/machine/scripts/scripts/subfolder/other-script new file mode 100755 index 000000000..47e740068 --- /dev/null +++ b/internal/policies/testdata/TestApplyPolicies/golden/succeed_if_checking_for_backend_online_status_returns_an_error/run/adsys/machine/scripts/scripts/subfolder/other-script @@ -0,0 +1 @@ +subfolder other script diff --git a/internal/policies/testdata/TestApplyPolicies/golden/succeed_if_checking_for_backend_online_status_returns_an_error/run/adsys/machine/scripts/scripts/unreferenced-data b/internal/policies/testdata/TestApplyPolicies/golden/succeed_if_checking_for_backend_online_status_returns_an_error/run/adsys/machine/scripts/scripts/unreferenced-data new file mode 100644 index 000000000..802d880a9 --- /dev/null +++ b/internal/policies/testdata/TestApplyPolicies/golden/succeed_if_checking_for_backend_online_status_returns_an_error/run/adsys/machine/scripts/scripts/unreferenced-data @@ -0,0 +1 @@ +unreferenced data diff --git a/internal/policies/testdata/TestApplyPolicies/golden/succeed_if_checking_for_backend_online_status_returns_an_error/run/adsys/machine/scripts/scripts/unreferenced-script b/internal/policies/testdata/TestApplyPolicies/golden/succeed_if_checking_for_backend_online_status_returns_an_error/run/adsys/machine/scripts/scripts/unreferenced-script new file mode 100644 index 000000000..be58cc792 --- /dev/null +++ b/internal/policies/testdata/TestApplyPolicies/golden/succeed_if_checking_for_backend_online_status_returns_an_error/run/adsys/machine/scripts/scripts/unreferenced-script @@ -0,0 +1 @@ +unreferenced script diff --git a/internal/policies/testdata/TestApplyPolicies/golden/succeed_if_checking_for_backend_online_status_returns_an_error/run/adsys/machine/scripts/shutdown b/internal/policies/testdata/TestApplyPolicies/golden/succeed_if_checking_for_backend_online_status_returns_an_error/run/adsys/machine/scripts/shutdown new file mode 100644 index 000000000..58c17cc29 --- /dev/null +++ b/internal/policies/testdata/TestApplyPolicies/golden/succeed_if_checking_for_backend_online_status_returns_an_error/run/adsys/machine/scripts/shutdown @@ -0,0 +1 @@ +scripts/script-machine-shutdown diff --git a/internal/policies/testdata/TestApplyPolicies/golden/succeed_if_checking_for_backend_online_status_returns_an_error/run/adsys/machine/scripts/startup b/internal/policies/testdata/TestApplyPolicies/golden/succeed_if_checking_for_backend_online_status_returns_an_error/run/adsys/machine/scripts/startup new file mode 100644 index 000000000..62ca19d92 --- /dev/null +++ b/internal/policies/testdata/TestApplyPolicies/golden/succeed_if_checking_for_backend_online_status_returns_an_error/run/adsys/machine/scripts/startup @@ -0,0 +1,3 @@ +scripts/script-machine-startup +scripts/subfolder/other-script +scripts/final-machine-script.sh diff --git a/internal/policies/testdata/TestApplyPolicies/golden/succeed_if_checking_for_backend_online_status_returns_an_error/run/adsys/users/.empty b/internal/policies/testdata/TestApplyPolicies/golden/succeed_if_checking_for_backend_online_status_returns_an_error/run/adsys/users/.empty new file mode 100644 index 000000000..e69de29bb diff --git a/internal/policies/testdata/TestApplyPolicies/golden/succeed_if_checking_for_backend_online_status_returns_an_error/sys/kernel/security/apparmor/profiles b/internal/policies/testdata/TestApplyPolicies/golden/succeed_if_checking_for_backend_online_status_returns_an_error/sys/kernel/security/apparmor/profiles new file mode 100644 index 000000000..bb27bcb87 --- /dev/null +++ b/internal/policies/testdata/TestApplyPolicies/golden/succeed_if_checking_for_backend_online_status_returns_an_error/sys/kernel/security/apparmor/profiles @@ -0,0 +1 @@ +someprofile (enforce) diff --git a/internal/policies/testdata/TestApplyPolicies/golden/succeed_if_checking_for_backend_online_status_returns_an_error/var/cache/adsys/policies/hostname/assets.db b/internal/policies/testdata/TestApplyPolicies/golden/succeed_if_checking_for_backend_online_status_returns_an_error/var/cache/adsys/policies/hostname/assets.db new file mode 100644 index 0000000000000000000000000000000000000000..3b52764c022f9d921653ce9d1c2be8c04afc71a5 GIT binary patch literal 4485 zcmb7{PiP!f9LFb3Vv{YEkR~LlQs-i!huO5C2&Fx=Qesn6S4cEca(KJ@CK=hC+0Gx@ zv}g@Tpa-Fc7FzM3s24?hX>Aq6qlaEw3O&_>#Y+VZc+eL7{pQV|-+MDVGadM4H}Kx) z`+L9Ny!XD_I59X>)9~4R^Uz}WCHga>y{MT^r_J31vrCJk+Te~Kx7eLq|FPig4sBrQ zk3mff|1>1R#*^UM^FY9uAq3`m-1iL2Hrke3_s{wX^72nxDpCyAEnA1dwNyZV@AXWI zUdy$5z9&+|^a@i%!oKA%ei?`KFfhfKAU=)28+}Z^^jJga(c>wKAm}8|I28<<&s?v^ zJ1;(*0vg7iTvpfzLoBASPO-Wk&d|jSlKR)m-kc_Kyxa5F4KGHBh94{2tGE%q>up}m zT|l^{jHLQLbN!%KxPWkmo)llqAPYdb^@W+GNvIVDZMJFItdpvRsJgdSq!&&}<-#e7 z+JpBW{XGFn7%z}=(LGh6HFS8kFFyoF7hjut^y{l2g0UTh<{FFj<0p@Y=}%w#mtiPJ z=BYVubC-2iYVUB@vmE>If&KeyZOiL**!oG<<%c7PZaS_mPD8T1@0GVDe&BS)3WOV zTxU%W78Di$@fb>?voK>8X3R#Zh#C6%50olQ{(Nv|3`8;TB}}Ri&5MPb>|7|5^!kKku&a)yP;QJUR2A=6}wBWMG_xv_DS&~gk<5H=9qaws6RO8AnhbC{p zFMznisr`^WPq{|c+6WEN=Ff;m*608ojZVRS&DtEgf(^b literal 0 HcmV?d00001 diff --git a/internal/policies/testdata/TestApplyPolicies/golden/succeed_if_checking_for_backend_online_status_returns_an_error/var/cache/adsys/policies/hostname/policies b/internal/policies/testdata/TestApplyPolicies/golden/succeed_if_checking_for_backend_online_status_returns_an_error/var/cache/adsys/policies/hostname/policies new file mode 100644 index 000000000..65c86435b --- /dev/null +++ b/internal/policies/testdata/TestApplyPolicies/golden/succeed_if_checking_for_backend_online_status_returns_an_error/var/cache/adsys/policies/hostname/policies @@ -0,0 +1,70 @@ +gpos: + - id: '{GPOId}' + name: GPOName + rules: + apparmor: + - key: apparmor-machine + value: | + usr.bin.foo + usr.bin.bar + nested/usr.bin.baz + disabled: false + dconf: + - key: path/to/key1 + value: ValueOfKey1 + disabled: false + meta: s + - key: path/to/key2 + value: | + ValueOfKey2 + On + Multilines + disabled: false + meta: s + mount: + - key: system-mounts + value: | + nfs://example.com/nfs_share + smb://example.com/smb_share + ftp://example.com/ftp_share + disabled: false + privilege: + - key: allow-local-admins + value: "" + disabled: false + - key: client-admins + value: | + alice@domain + bob@domain2 + %mygroup@domain + cosmic carole@domain + disabled: false + proxy: + - key: proxy/auto + value: http://example.com/proxy.pac + disabled: false + - key: proxy/http + value: "" + disabled: true + - key: proxy/no-proxy + value: localhost,127.0.0.1,::1 + disabled: false + scripts: + - key: startup + value: | + script-machine-startup + subfolder/other-script + final-machine-script.sh + disabled: false + - key: shutdown + value: | + script-machine-shutdown + disabled: false + - key: logon + value: | + script-user-logon + disabled: false + - key: logoff + value: | + otherfolder/script-user-logoff + disabled: false From 170bcfd20c33ecef71a2a628059b6308f927a5e2 Mon Sep 17 00:00:00 2001 From: Gabriel Nagy Date: Wed, 26 Jul 2023 02:26:28 +0300 Subject: [PATCH 05/13] Add CLI command to dump certificate enroll script Similar to the adsys-gpolist, provide a way for users to dump the certificate autoenrollment script for debugging purposes. --- adsys.pb.go | 40 +++++++----- adsys.proto | 1 + adsys_grpc.pb.go | 64 +++++++++++++++++++ cmd/adsysd/client/policy.go | 28 ++++++++ .../integration_tests/adsysctl_policy_test.go | 33 ++++++---- internal/adsysservice/policy.go | 18 ++++++ 6 files changed, 154 insertions(+), 30 deletions(-) diff --git a/adsys.pb.go b/adsys.pb.go index 1b84188d6..9a5ddae6e 100644 --- a/adsys.pb.go +++ b/adsys.pb.go @@ -596,7 +596,7 @@ var file_adsys_proto_rawDesc = []byte{ 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x63, 0x68, 0x61, 0x70, 0x74, 0x65, 0x72, 0x22, 0x22, 0x0a, 0x0e, 0x4c, 0x69, 0x73, 0x74, 0x44, 0x6f, 0x63, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x10, 0x0a, 0x03, 0x72, 0x61, 0x77, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x03, 0x72, - 0x61, 0x77, 0x32, 0x96, 0x04, 0x0a, 0x07, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x20, + 0x61, 0x77, 0x32, 0xc9, 0x04, 0x0a, 0x07, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x20, 0x0a, 0x03, 0x43, 0x61, 0x74, 0x12, 0x06, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x0f, 0x2e, 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x30, 0x01, 0x12, 0x24, 0x0a, 0x07, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x06, 0x2e, 0x45, 0x6d, @@ -629,9 +629,13 @@ var file_adsys_proto_rawDesc = []byte{ 0x74, 0x72, 0x69, 0x6e, 0x67, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x30, 0x01, 0x12, 0x2a, 0x0a, 0x0d, 0x47, 0x50, 0x4f, 0x4c, 0x69, 0x73, 0x74, 0x53, 0x63, 0x72, 0x69, 0x70, 0x74, 0x12, 0x06, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x0f, 0x2e, 0x53, 0x74, 0x72, 0x69, 0x6e, - 0x67, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x30, 0x01, 0x42, 0x19, 0x5a, 0x17, 0x67, - 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x75, 0x62, 0x75, 0x6e, 0x74, 0x75, - 0x2f, 0x61, 0x64, 0x73, 0x79, 0x73, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x67, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x30, 0x01, 0x12, 0x31, 0x0a, 0x14, 0x43, + 0x65, 0x72, 0x74, 0x41, 0x75, 0x74, 0x6f, 0x45, 0x6e, 0x72, 0x6f, 0x6c, 0x6c, 0x53, 0x63, 0x72, + 0x69, 0x70, 0x74, 0x12, 0x06, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x0f, 0x2e, 0x53, 0x74, + 0x72, 0x69, 0x6e, 0x67, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x30, 0x01, 0x42, 0x19, + 0x5a, 0x17, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x75, 0x62, 0x75, + 0x6e, 0x74, 0x75, 0x2f, 0x61, 0x64, 0x73, 0x79, 0x73, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, + 0x33, } var ( @@ -671,19 +675,21 @@ var file_adsys_proto_depIdxs = []int32{ 9, // 8: service.ListDoc:input_type -> ListDocRequest 1, // 9: service.ListUsers:input_type -> ListUsersRequest 0, // 10: service.GPOListScript:input_type -> Empty - 3, // 11: service.Cat:output_type -> StringResponse - 3, // 12: service.Version:output_type -> StringResponse - 3, // 13: service.Status:output_type -> StringResponse - 0, // 14: service.Stop:output_type -> Empty - 0, // 15: service.UpdatePolicy:output_type -> Empty - 3, // 16: service.DumpPolicies:output_type -> StringResponse - 7, // 17: service.DumpPoliciesDefinitions:output_type -> DumpPolicyDefinitionsResponse - 3, // 18: service.GetDoc:output_type -> StringResponse - 3, // 19: service.ListDoc:output_type -> StringResponse - 3, // 20: service.ListUsers:output_type -> StringResponse - 3, // 21: service.GPOListScript:output_type -> StringResponse - 11, // [11:22] is the sub-list for method output_type - 0, // [0:11] is the sub-list for method input_type + 0, // 11: service.CertAutoEnrollScript:input_type -> Empty + 3, // 12: service.Cat:output_type -> StringResponse + 3, // 13: service.Version:output_type -> StringResponse + 3, // 14: service.Status:output_type -> StringResponse + 0, // 15: service.Stop:output_type -> Empty + 0, // 16: service.UpdatePolicy:output_type -> Empty + 3, // 17: service.DumpPolicies:output_type -> StringResponse + 7, // 18: service.DumpPoliciesDefinitions:output_type -> DumpPolicyDefinitionsResponse + 3, // 19: service.GetDoc:output_type -> StringResponse + 3, // 20: service.ListDoc:output_type -> StringResponse + 3, // 21: service.ListUsers:output_type -> StringResponse + 3, // 22: service.GPOListScript:output_type -> StringResponse + 3, // 23: service.CertAutoEnrollScript:output_type -> StringResponse + 12, // [12:24] is the sub-list for method output_type + 0, // [0:12] is the sub-list for method input_type 0, // [0:0] is the sub-list for extension type_name 0, // [0:0] is the sub-list for extension extendee 0, // [0:0] is the sub-list for field type_name diff --git a/adsys.proto b/adsys.proto index cc95a16a2..307b28ca5 100644 --- a/adsys.proto +++ b/adsys.proto @@ -14,6 +14,7 @@ service service { rpc ListDoc(ListDocRequest) returns (stream StringResponse); rpc ListUsers(ListUsersRequest) returns (stream StringResponse); rpc GPOListScript(Empty) returns (stream StringResponse); + rpc CertAutoEnrollScript(Empty) returns (stream StringResponse); } message Empty {} diff --git a/adsys_grpc.pb.go b/adsys_grpc.pb.go index 787185740..1bc7b2b16 100644 --- a/adsys_grpc.pb.go +++ b/adsys_grpc.pb.go @@ -30,6 +30,7 @@ const ( Service_ListDoc_FullMethodName = "/service/ListDoc" Service_ListUsers_FullMethodName = "/service/ListUsers" Service_GPOListScript_FullMethodName = "/service/GPOListScript" + Service_CertAutoEnrollScript_FullMethodName = "/service/CertAutoEnrollScript" ) // ServiceClient is the client API for Service service. @@ -47,6 +48,7 @@ type ServiceClient interface { ListDoc(ctx context.Context, in *ListDocRequest, opts ...grpc.CallOption) (Service_ListDocClient, error) ListUsers(ctx context.Context, in *ListUsersRequest, opts ...grpc.CallOption) (Service_ListUsersClient, error) GPOListScript(ctx context.Context, in *Empty, opts ...grpc.CallOption) (Service_GPOListScriptClient, error) + CertAutoEnrollScript(ctx context.Context, in *Empty, opts ...grpc.CallOption) (Service_CertAutoEnrollScriptClient, error) } type serviceClient struct { @@ -409,6 +411,38 @@ func (x *serviceGPOListScriptClient) Recv() (*StringResponse, error) { return m, nil } +func (c *serviceClient) CertAutoEnrollScript(ctx context.Context, in *Empty, opts ...grpc.CallOption) (Service_CertAutoEnrollScriptClient, error) { + stream, err := c.cc.NewStream(ctx, &Service_ServiceDesc.Streams[11], Service_CertAutoEnrollScript_FullMethodName, opts...) + if err != nil { + return nil, err + } + x := &serviceCertAutoEnrollScriptClient{stream} + if err := x.ClientStream.SendMsg(in); err != nil { + return nil, err + } + if err := x.ClientStream.CloseSend(); err != nil { + return nil, err + } + return x, nil +} + +type Service_CertAutoEnrollScriptClient interface { + Recv() (*StringResponse, error) + grpc.ClientStream +} + +type serviceCertAutoEnrollScriptClient struct { + grpc.ClientStream +} + +func (x *serviceCertAutoEnrollScriptClient) Recv() (*StringResponse, error) { + m := new(StringResponse) + if err := x.ClientStream.RecvMsg(m); err != nil { + return nil, err + } + return m, nil +} + // ServiceServer is the server API for Service service. // All implementations must embed UnimplementedServiceServer // for forward compatibility @@ -424,6 +458,7 @@ type ServiceServer interface { ListDoc(*ListDocRequest, Service_ListDocServer) error ListUsers(*ListUsersRequest, Service_ListUsersServer) error GPOListScript(*Empty, Service_GPOListScriptServer) error + CertAutoEnrollScript(*Empty, Service_CertAutoEnrollScriptServer) error mustEmbedUnimplementedServiceServer() } @@ -464,6 +499,9 @@ func (UnimplementedServiceServer) ListUsers(*ListUsersRequest, Service_ListUsers func (UnimplementedServiceServer) GPOListScript(*Empty, Service_GPOListScriptServer) error { return status.Errorf(codes.Unimplemented, "method GPOListScript not implemented") } +func (UnimplementedServiceServer) CertAutoEnrollScript(*Empty, Service_CertAutoEnrollScriptServer) error { + return status.Errorf(codes.Unimplemented, "method CertAutoEnrollScript not implemented") +} func (UnimplementedServiceServer) mustEmbedUnimplementedServiceServer() {} // UnsafeServiceServer may be embedded to opt out of forward compatibility for this service. @@ -708,6 +746,27 @@ func (x *serviceGPOListScriptServer) Send(m *StringResponse) error { return x.ServerStream.SendMsg(m) } +func _Service_CertAutoEnrollScript_Handler(srv interface{}, stream grpc.ServerStream) error { + m := new(Empty) + if err := stream.RecvMsg(m); err != nil { + return err + } + return srv.(ServiceServer).CertAutoEnrollScript(m, &serviceCertAutoEnrollScriptServer{stream}) +} + +type Service_CertAutoEnrollScriptServer interface { + Send(*StringResponse) error + grpc.ServerStream +} + +type serviceCertAutoEnrollScriptServer struct { + grpc.ServerStream +} + +func (x *serviceCertAutoEnrollScriptServer) Send(m *StringResponse) error { + return x.ServerStream.SendMsg(m) +} + // Service_ServiceDesc is the grpc.ServiceDesc for Service service. // It's only intended for direct use with grpc.RegisterService, // and not to be introspected or modified (even as a copy) @@ -771,6 +830,11 @@ var Service_ServiceDesc = grpc.ServiceDesc{ Handler: _Service_GPOListScript_Handler, ServerStreams: true, }, + { + StreamName: "CertAutoEnrollScript", + Handler: _Service_CertAutoEnrollScript_Handler, + ServerStreams: true, + }, }, Metadata: "adsys.proto", } diff --git a/cmd/adsysd/client/policy.go b/cmd/adsysd/client/policy.go index 0d90ad3db..db8ef39e4 100644 --- a/cmd/adsysd/client/policy.go +++ b/cmd/adsysd/client/policy.go @@ -86,6 +86,14 @@ func (a *App) installPolicy() { RunE: func(cmd *cobra.Command, args []string) error { return a.dumpGPOListScript() }, } debugCmd.AddCommand(gpoListCmd) + certEnrollCmd := &cobra.Command{ + Use: "cert-autoenroll-script", + Short: gotext.Get("Write certificate autoenrollment python embedded script in current directory"), + Args: cobra.NoArgs, + ValidArgsFunction: cmdhandler.NoValidArgs, + RunE: func(cmd *cobra.Command, args []string) error { return a.dumpCertEnrollScript() }, + } + debugCmd.AddCommand(certEnrollCmd) var updateMachine, updateAll *bool updateCmd := &cobra.Command{ @@ -271,6 +279,26 @@ func (a *App) dumpGPOListScript() error { return os.WriteFile("adsys-gpolist", []byte(script), 0600) } +func (a *App) dumpCertEnrollScript() error { + client, err := adsysservice.NewClient(a.config.Socket, a.getTimeout()) + if err != nil { + return err + } + defer client.Close() + + stream, err := client.CertAutoEnrollScript(a.ctx, &adsys.Empty{}) + if err != nil { + return err + } + + script, err := singleMsg(stream) + if err != nil { + return err + } + + return os.WriteFile("cert-autoenroll", []byte(script), 0600) +} + func colorizePolicies(policies string) (string, error) { first := true var out stringsBuilderWithError diff --git a/cmd/adsysd/integration_tests/adsysctl_policy_test.go b/cmd/adsysd/integration_tests/adsysctl_policy_test.go index f2b2fe958..aa5908006 100644 --- a/cmd/adsysd/integration_tests/adsysctl_policy_test.go +++ b/cmd/adsysd/integration_tests/adsysctl_policy_test.go @@ -1088,26 +1088,33 @@ func TestPolicyUpdate(t *testing.T) { } } -func TestPolicyDebugGPOListScript(t *testing.T) { - gpolistSrc, err := os.ReadFile(filepath.Join(rootProjectDir, "internal/ad/adsys-gpolist")) - require.NoError(t, err, "Setup: failed to load source of adsys-gpolist") - +func TestPolicyDebugScriptDump(t *testing.T) { tests := map[string]struct { + script string + cmdName string + path string + systemAnswer string daemonNotStarted bool wantErr bool }{ - "Get adsys-gpolist script": {systemAnswer: "polkit_yes"}, - "Version is always authorized": {systemAnswer: "polkit_no"}, + "Get adsys-gpolist script": {script: "adsys-gpolist", cmdName: "gpolist-script", path: "internal/ad", systemAnswer: "polkit_yes"}, + "Get cert-autoenroll script": {script: "cert-autoenroll", cmdName: "cert-autoenroll-script", path: "internal/policies/certificate", systemAnswer: "polkit_yes"}, + "adsys-gpolist is always authorized": {script: "adsys-gpolist", cmdName: "gpolist-script", path: "internal/ad", systemAnswer: "polkit_no"}, + "cert-autoenroll is always authorized": {script: "cert-autoenroll", cmdName: "cert-autoenroll-script", path: "internal/policies/certificate", systemAnswer: "polkit_no"}, - "Error on daemon not responding": {daemonNotStarted: true, wantErr: true}, + "Error on daemon not responding for adsys-gpolist": {script: "adsys-gpolist", cmdName: "gpolist-script", path: "internal/ad", daemonNotStarted: true, wantErr: true}, + "Error on daemon not responding for cert-autoenroll": {script: "cert-autoenroll", cmdName: "cert-autoenroll-script", path: "internal/policies/certificate", daemonNotStarted: true, wantErr: true}, } for name, tc := range tests { tc := tc t.Run(name, func(t *testing.T) { dbusAnswer(t, tc.systemAnswer) + scriptSrc, err := os.ReadFile(filepath.Join(rootProjectDir, tc.path, tc.script)) + require.NoError(t, err, "Setup: failed to load source of %s script", tc.script) + conf := createConf(t) if !tc.daemonNotStarted { defer runDaemon(t, conf)() @@ -1115,21 +1122,21 @@ func TestPolicyDebugGPOListScript(t *testing.T) { testutils.Chdir(t, os.TempDir()) - _, err := runClient(t, conf, "policy", "debug", "gpolist-script") + _, err = runClient(t, conf, "policy", "debug", tc.cmdName) if tc.wantErr { require.Error(t, err, "client should exit with an error") return } - f, err := os.Stat("adsys-gpolist") - require.NoError(t, err, "gpo list script should exists") + f, err := os.Stat(tc.script) + require.NoError(t, err, "%s script should exists", tc.script) require.NotEqual(t, 0, f.Mode()&0111, "Script should be executable") - got, err := os.ReadFile("adsys-gpolist") - require.NoError(t, err, "gpo list script is not readable") + got, err := os.ReadFile(tc.script) + require.NoError(t, err, "%s script is not readable", tc.script) - require.Equal(t, string(gpolistSrc), string(got), "Script content should match source") + require.Equal(t, string(scriptSrc), string(got), "Script content should match source") }) } } diff --git a/internal/adsysservice/policy.go b/internal/adsysservice/policy.go index ab498e398..f30e0ca46 100644 --- a/internal/adsysservice/policy.go +++ b/internal/adsysservice/policy.go @@ -11,6 +11,7 @@ import ( log "github.com/ubuntu/adsys/internal/grpc/logstreamer" "github.com/ubuntu/adsys/internal/i18n" "github.com/ubuntu/adsys/internal/policies" + "github.com/ubuntu/adsys/internal/policies/certificate" "github.com/ubuntu/decorate" "golang.org/x/sync/errgroup" ) @@ -156,4 +157,21 @@ func (s *Service) GPOListScript(_ *adsys.Empty, stream adsys.Service_GPOListScri return nil } +// CertAutoEnrollScript returns the embedded certificate autoenrollment python script. +func (s *Service) CertAutoEnrollScript(_ *adsys.Empty, stream adsys.Service_CertAutoEnrollScriptServer) (err error) { + defer decorate.OnError(&err, i18n.G("error while getting certificate autoenrollment script")) + + if err := s.authorizer.IsAllowedFromContext(stream.Context(), authorizer.ActionAlwaysAllowed); err != nil { + return err + } + + if err := stream.Send(&adsys.StringResponse{ + Msg: certificate.CertEnrollCode, + }); err != nil { + log.Warningf(stream.Context(), "couldn't send certificate autoenrollment script to client: %v", err) + } + + return nil +} + // FIXME: check cache file permission From af7aca4c7a02de71221f731b5649bd41d7fb60d8 Mon Sep 17 00:00:00 2001 From: Gabriel Nagy Date: Fri, 28 Jul 2023 11:33:03 +0300 Subject: [PATCH 06/13] Match Samba permissions when creating directories The Samba implementation creates the private directory with 0700 permissions and uses 0755 for the rest. We should honor that in our implementation as well. Additionally, create the global trust dir on the off chance it doesn't already exist. --- internal/policies/certificate/cert-autoenroll | 5 +++-- .../golden/enroll_with_empty_advanced_configuration | 6 +++--- .../golden/enroll_with_simple_configuration | 6 +++--- .../enroll_with_simple_configuration_and_debug_enabled | 6 +++--- .../golden/enroll_with_valid_advanced_configuration | 6 +++--- .../admock/vendor_samba/gp/gp_cert_auto_enroll_ext.py | 6 +++--- 6 files changed, 18 insertions(+), 17 deletions(-) diff --git a/internal/policies/certificate/cert-autoenroll b/internal/policies/certificate/cert-autoenroll index 02e0a3a35..1f34f6ff4 100755 --- a/internal/policies/certificate/cert-autoenroll +++ b/internal/policies/certificate/cert-autoenroll @@ -65,9 +65,10 @@ def main(): global_trust_dir = args.global_trust_dir # Create needed directories if they don't exist - for directory in [state_dir, trust_dir, private_dir]: + for directory in [state_dir, trust_dir, private_dir, global_trust_dir]: if not os.path.exists(directory): - os.makedirs(directory) + perms = 0o700 if directory == private_dir else 0o755 + os.makedirs(directory, mode=perms) with tempfile.NamedTemporaryFile(prefix='smb_conf') as smb_conf: smb_conf.write(smb_config(args.realm, args.debug).encode('utf-8')) diff --git a/internal/policies/certificate/testdata/TestCertAutoenrollScript/golden/enroll_with_empty_advanced_configuration b/internal/policies/certificate/testdata/TestCertAutoenrollScript/golden/enroll_with_empty_advanced_configuration index a031f00f8..d64213e44 100644 --- a/internal/policies/certificate/testdata/TestCertAutoenrollScript/golden/enroll_with_empty_advanced_configuration +++ b/internal/policies/certificate/testdata/TestCertAutoenrollScript/golden/enroll_with_empty_advanced_configuration @@ -6,6 +6,6 @@ Loading state file: #TMPDIR#/state/cert_gpo_state_keypress.tdb Enroll called guid: adsys-cert-autoenroll-keypress -trust_dir: #TMPDIR#/trust; exists: True -private_dir: #TMPDIR#/private; exists: True -global_trust_dir: #TMPDIR#/ca-certificates; exists: False +trust_dir: #TMPDIR#/trust; mode: 0o40755 +private_dir: #TMPDIR#/private; mode: 0o40700 +global_trust_dir: #TMPDIR#/ca-certificates; mode: 0o40755 diff --git a/internal/policies/certificate/testdata/TestCertAutoenrollScript/golden/enroll_with_simple_configuration b/internal/policies/certificate/testdata/TestCertAutoenrollScript/golden/enroll_with_simple_configuration index a031f00f8..d64213e44 100644 --- a/internal/policies/certificate/testdata/TestCertAutoenrollScript/golden/enroll_with_simple_configuration +++ b/internal/policies/certificate/testdata/TestCertAutoenrollScript/golden/enroll_with_simple_configuration @@ -6,6 +6,6 @@ Loading state file: #TMPDIR#/state/cert_gpo_state_keypress.tdb Enroll called guid: adsys-cert-autoenroll-keypress -trust_dir: #TMPDIR#/trust; exists: True -private_dir: #TMPDIR#/private; exists: True -global_trust_dir: #TMPDIR#/ca-certificates; exists: False +trust_dir: #TMPDIR#/trust; mode: 0o40755 +private_dir: #TMPDIR#/private; mode: 0o40700 +global_trust_dir: #TMPDIR#/ca-certificates; mode: 0o40755 diff --git a/internal/policies/certificate/testdata/TestCertAutoenrollScript/golden/enroll_with_simple_configuration_and_debug_enabled b/internal/policies/certificate/testdata/TestCertAutoenrollScript/golden/enroll_with_simple_configuration_and_debug_enabled index 5e283063c..3876ae0a7 100644 --- a/internal/policies/certificate/testdata/TestCertAutoenrollScript/golden/enroll_with_simple_configuration_and_debug_enabled +++ b/internal/policies/certificate/testdata/TestCertAutoenrollScript/golden/enroll_with_simple_configuration_and_debug_enabled @@ -7,6 +7,6 @@ Loading state file: #TMPDIR#/state/cert_gpo_state_keypress.tdb Enroll called guid: adsys-cert-autoenroll-keypress -trust_dir: #TMPDIR#/trust; exists: True -private_dir: #TMPDIR#/private; exists: True -global_trust_dir: #TMPDIR#/ca-certificates; exists: False +trust_dir: #TMPDIR#/trust; mode: 0o40755 +private_dir: #TMPDIR#/private; mode: 0o40700 +global_trust_dir: #TMPDIR#/ca-certificates; mode: 0o40755 diff --git a/internal/policies/certificate/testdata/TestCertAutoenrollScript/golden/enroll_with_valid_advanced_configuration b/internal/policies/certificate/testdata/TestCertAutoenrollScript/golden/enroll_with_valid_advanced_configuration index 9fe4ef1bf..f2d1af1de 100644 --- a/internal/policies/certificate/testdata/TestCertAutoenrollScript/golden/enroll_with_valid_advanced_configuration +++ b/internal/policies/certificate/testdata/TestCertAutoenrollScript/golden/enroll_with_valid_advanced_configuration @@ -6,9 +6,9 @@ Loading state file: #TMPDIR#/state/cert_gpo_state_keypress.tdb Enroll called guid: adsys-cert-autoenroll-keypress -trust_dir: #TMPDIR#/trust; exists: True -private_dir: #TMPDIR#/private; exists: True -global_trust_dir: #TMPDIR#/ca-certificates; exists: False +trust_dir: #TMPDIR#/trust; mode: 0o40755 +private_dir: #TMPDIR#/private; mode: 0o40700 +global_trust_dir: #TMPDIR#/ca-certificates; mode: 0o40755 entries: keyname: Software\Policies\Microsoft\Cryptography\PolicyServers\37c9dc30f207f27f61a2f7c3aed598a6e2920b54 diff --git a/internal/testutils/admock/vendor_samba/gp/gp_cert_auto_enroll_ext.py b/internal/testutils/admock/vendor_samba/gp/gp_cert_auto_enroll_ext.py index 31f2dbcaa..04de011af 100644 --- a/internal/testutils/admock/vendor_samba/gp/gp_cert_auto_enroll_ext.py +++ b/internal/testutils/admock/vendor_samba/gp/gp_cert_auto_enroll_ext.py @@ -14,9 +14,9 @@ def __enroll(self, guid, entries, trust_dir, private_dir, global_trust_dir): print('Enroll called') print() print(f'guid: {guid}') - print(f'trust_dir: {trust_dir}; exists: {os.path.exists(trust_dir)}') - print(f'private_dir: {private_dir}; exists: {os.path.exists(private_dir)}') - print(f'global_trust_dir: {global_trust_dir}; exists: {os.path.exists(global_trust_dir)}') + print(f'trust_dir: {trust_dir}; mode: {oct(os.stat(trust_dir).st_mode)}') + print(f'private_dir: {private_dir}; mode: {oct(os.stat(private_dir).st_mode)}') + print(f'global_trust_dir: {global_trust_dir}; mode: {oct(os.stat(global_trust_dir).st_mode)}') if entries == []: return From 2c1cfcc01ad68445be584679be4a13ee7f6184c6 Mon Sep 17 00:00:00 2001 From: Gabriel Nagy Date: Fri, 28 Jul 2023 11:36:18 +0300 Subject: [PATCH 07/13] Remove unused Python import --- internal/policies/certificate/cert-autoenroll | 2 -- 1 file changed, 2 deletions(-) diff --git a/internal/policies/certificate/cert-autoenroll b/internal/policies/certificate/cert-autoenroll index 1f34f6ff4..1f3614c1a 100755 --- a/internal/policies/certificate/cert-autoenroll +++ b/internal/policies/certificate/cert-autoenroll @@ -6,8 +6,6 @@ import os import sys import tempfile -from collections import namedtuple - from samba import param from samba.credentials import MUST_USE_KERBEROS, Credentials from samba.dcerpc import preg From b49fcd0ac623de9b4509a43452a8fa4c4a24f75e Mon Sep 17 00:00:00 2001 From: Gabriel Nagy Date: Fri, 28 Jul 2023 11:37:27 +0300 Subject: [PATCH 08/13] Write Samba cache file to a specific directory And change the naming from stateDir to sambaCacheDir to better reflect what the path is used for. --- internal/policies/certificate/cert-autoenroll | 12 ++++++------ .../policies/certificate/cert-autoenroll_test.go | 2 +- internal/policies/certificate/certificate.go | 6 +++--- .../golden/computer,_configured_to_enroll | 2 +- ...ter,_configured_to_enroll,_advanced_configuration | 2 +- .../golden/computer,_configured_to_unenroll | 2 +- 6 files changed, 13 insertions(+), 13 deletions(-) diff --git a/internal/policies/certificate/cert-autoenroll b/internal/policies/certificate/cert-autoenroll index 1f3614c1a..03ec44731 100755 --- a/internal/policies/certificate/cert-autoenroll +++ b/internal/policies/certificate/cert-autoenroll @@ -40,9 +40,9 @@ def main(): parser.add_argument('--policy_servers_json', type=str, help='GPO entries for advanced configuration of the policy servers. \ Must be in JSON format.') - parser.add_argument('--state_dir', type=str, - default='/var/lib/adsys', - help='Directory to store GPO state in.') + parser.add_argument('--samba_cache_dir', type=str, + default='/var/lib/adsys/samba', + help='Directory to store GPO Samba cache in.') parser.add_argument('--private_dir', type=str, default='/var/lib/adsys/private/certs', help='Directory to store private keys in.') @@ -57,13 +57,13 @@ def main(): args = parser.parse_args() - state_dir = args.state_dir + samba_cache_dir = args.samba_cache_dir trust_dir = args.trust_dir private_dir = args.private_dir global_trust_dir = args.global_trust_dir # Create needed directories if they don't exist - for directory in [state_dir, trust_dir, private_dir, global_trust_dir]: + for directory in [samba_cache_dir, trust_dir, private_dir, global_trust_dir]: if not os.path.exists(directory): perms = 0o700 if directory == private_dir else 0o755 os.makedirs(directory, mode=perms) @@ -77,7 +77,7 @@ def main(): c.set_kerberos_state(MUST_USE_KERBEROS) c.guess(lp) username = c.get_username() - store = GPOStorage(os.path.join(state_dir, f'cert_gpo_state_{args.object_name}.tdb')) + store = GPOStorage(os.path.join(samba_cache_dir, f'cert_gpo_state_{args.object_name}.tdb')) ext = adsys_cert_auto_enroll(lp, c, username, store) guid = f'adsys-cert-autoenroll-{args.object_name}' diff --git a/internal/policies/certificate/cert-autoenroll_test.go b/internal/policies/certificate/cert-autoenroll_test.go index 8899c35d9..011f5d004 100644 --- a/internal/policies/certificate/cert-autoenroll_test.go +++ b/internal/policies/certificate/cert-autoenroll_test.go @@ -119,7 +119,7 @@ func TestCertAutoenrollScript(t *testing.T) { testutils.MakeReadOnly(t, tmpdir) } - args := append(tc.args, "--state_dir", stateDir, "--private_dir", privateDir, "--trust_dir", trustDir, "--global_trust_dir", globalTrustDir) + args := append(tc.args, "--samba_cache_dir", stateDir, "--private_dir", privateDir, "--trust_dir", trustDir, "--global_trust_dir", globalTrustDir) // #nosec G204: we control the command line name and only change it for tests cmd := exec.Command(certAutoenrollCmd, args...) diff --git a/internal/policies/certificate/certificate.go b/internal/policies/certificate/certificate.go index a1a05507d..cc0c5858c 100644 --- a/internal/policies/certificate/certificate.go +++ b/internal/policies/certificate/certificate.go @@ -42,7 +42,7 @@ import ( // the policy in ApplyPolicy. type Manager struct { domain string - stateDir string + sambaCacheDir string krb5CacheDir string vendorPythonDir string certEnrollCmd []string @@ -131,7 +131,7 @@ func New(domain string, opts ...Option) *Manager { return &Manager{ domain: domain, - stateDir: args.stateDir, + sambaCacheDir: filepath.Join(args.stateDir, "samba"), krb5CacheDir: filepath.Join(args.runDir, "krb5cc"), vendorPythonDir: filepath.Join(args.shareDir, "python"), certEnrollCmd: args.certAutoenrollCmd, @@ -204,7 +204,7 @@ func (m *Manager) ApplyPolicy(ctx context.Context, objectName string, isComputer if err := m.runScript(ctx, action, objectName, "--policy_servers_json", string(jsonGPOData), - "--state_dir", m.stateDir, + "--samba_cache_dir", m.sambaCacheDir, ); err != nil { return err } diff --git a/internal/policies/certificate/testdata/TestPolicyApply/golden/computer,_configured_to_enroll b/internal/policies/certificate/testdata/TestPolicyApply/golden/computer,_configured_to_enroll index b53aaaf88..1dbbbe06a 100644 --- a/internal/policies/certificate/testdata/TestPolicyApply/golden/computer,_configured_to_enroll +++ b/internal/policies/certificate/testdata/TestPolicyApply/golden/computer,_configured_to_enroll @@ -1,3 +1,3 @@ -enroll keypress example.com --policy_servers_json null --state_dir #TMPDIR#/statedir +enroll keypress example.com --policy_servers_json null --samba_cache_dir #TMPDIR#/statedir/samba KRB5CCNAME=#TMPDIR#/rundir/krb5cc/keypress PYTHONPATH=#TMPDIR#/sharedir/python diff --git a/internal/policies/certificate/testdata/TestPolicyApply/golden/computer,_configured_to_enroll,_advanced_configuration b/internal/policies/certificate/testdata/TestPolicyApply/golden/computer,_configured_to_enroll,_advanced_configuration index ebedab266..7af6802c2 100644 --- a/internal/policies/certificate/testdata/TestPolicyApply/golden/computer,_configured_to_enroll,_advanced_configuration +++ b/internal/policies/certificate/testdata/TestPolicyApply/golden/computer,_configured_to_enroll,_advanced_configuration @@ -1,3 +1,3 @@ -enroll keypress example.com --policy_servers_json [{"keyname":"Software\\Policies\\Microsoft\\Cryptography\\PolicyServers\\37c9dc30f207f27f61a2f7c3aed598a6e2920b54","valuename":"AuthFlags","data":2,"type":4},{"keyname":"Software\\Policies\\Microsoft\\Cryptography\\PolicyServers\\37c9dc30f207f27f61a2f7c3aed598a6e2920b54","valuename":"Cost","data":2147483645,"type":4},{"keyname":"Software\\Policies\\Microsoft\\Cryptography\\PolicyServers\\37c9dc30f207f27f61a2f7c3aed598a6e2920b54","valuename":"Flags","data":20,"type":4},{"keyname":"Software\\Policies\\Microsoft\\Cryptography\\PolicyServers\\37c9dc30f207f27f61a2f7c3aed598a6e2920b54","valuename":"FriendlyName","data":"ActiveDirectoryEnrollmentPolicy","type":1},{"keyname":"Software\\Policies\\Microsoft\\Cryptography\\PolicyServers\\37c9dc30f207f27f61a2f7c3aed598a6e2920b54","valuename":"PolicyID","data":"{A5E9BF57-71C6-443A-B7FC-79EFA6F73EBD}","type":1},{"keyname":"Software\\Policies\\Microsoft\\Cryptography\\PolicyServers\\37c9dc30f207f27f61a2f7c3aed598a6e2920b54","valuename":"URL","data":"LDAP:","type":1},{"keyname":"Software\\Policies\\Microsoft\\Cryptography\\PolicyServers","valuename":"Flags","data":0,"type":4}] --state_dir #TMPDIR#/statedir +enroll keypress example.com --policy_servers_json [{"keyname":"Software\\Policies\\Microsoft\\Cryptography\\PolicyServers\\37c9dc30f207f27f61a2f7c3aed598a6e2920b54","valuename":"AuthFlags","data":2,"type":4},{"keyname":"Software\\Policies\\Microsoft\\Cryptography\\PolicyServers\\37c9dc30f207f27f61a2f7c3aed598a6e2920b54","valuename":"Cost","data":2147483645,"type":4},{"keyname":"Software\\Policies\\Microsoft\\Cryptography\\PolicyServers\\37c9dc30f207f27f61a2f7c3aed598a6e2920b54","valuename":"Flags","data":20,"type":4},{"keyname":"Software\\Policies\\Microsoft\\Cryptography\\PolicyServers\\37c9dc30f207f27f61a2f7c3aed598a6e2920b54","valuename":"FriendlyName","data":"ActiveDirectoryEnrollmentPolicy","type":1},{"keyname":"Software\\Policies\\Microsoft\\Cryptography\\PolicyServers\\37c9dc30f207f27f61a2f7c3aed598a6e2920b54","valuename":"PolicyID","data":"{A5E9BF57-71C6-443A-B7FC-79EFA6F73EBD}","type":1},{"keyname":"Software\\Policies\\Microsoft\\Cryptography\\PolicyServers\\37c9dc30f207f27f61a2f7c3aed598a6e2920b54","valuename":"URL","data":"LDAP:","type":1},{"keyname":"Software\\Policies\\Microsoft\\Cryptography\\PolicyServers","valuename":"Flags","data":0,"type":4}] --samba_cache_dir #TMPDIR#/statedir/samba KRB5CCNAME=#TMPDIR#/rundir/krb5cc/keypress PYTHONPATH=#TMPDIR#/sharedir/python diff --git a/internal/policies/certificate/testdata/TestPolicyApply/golden/computer,_configured_to_unenroll b/internal/policies/certificate/testdata/TestPolicyApply/golden/computer,_configured_to_unenroll index 87bdd0251..5d4686e66 100644 --- a/internal/policies/certificate/testdata/TestPolicyApply/golden/computer,_configured_to_unenroll +++ b/internal/policies/certificate/testdata/TestPolicyApply/golden/computer,_configured_to_unenroll @@ -1,3 +1,3 @@ -unenroll keypress example.com --policy_servers_json null --state_dir #TMPDIR#/statedir +unenroll keypress example.com --policy_servers_json null --samba_cache_dir #TMPDIR#/statedir/samba KRB5CCNAME=#TMPDIR#/rundir/krb5cc/keypress PYTHONPATH=#TMPDIR#/sharedir/python From 7c810ffd1fbfb0fa6d131c2dc9062b69d2271067 Mon Sep 17 00:00:00 2001 From: Gabriel Nagy Date: Fri, 28 Jul 2023 12:27:13 +0300 Subject: [PATCH 09/13] Unenroll machine if cert policy is not configured Similar to our other policy manager behaviors, if the certificate policy is either disabled or not configured, unenroll the machine if applicable. To avoid running the helper Python script on every policy apply, determine if we actually need to unenroll by checking the existence of the Samba cache directory. Additionally, update the Python script to remove the directory after unenrollment is successful. --- internal/policies/certificate/cert-autoenroll | 1 + .../certificate/cert-autoenroll_test.go | 5 +++ internal/policies/certificate/certificate.go | 31 ++++++++++++------- .../policies/certificate/certificate_test.go | 20 +++++++++--- .../computer,_no_entries,_samba_cache_present | 3 ++ 5 files changed, 45 insertions(+), 15 deletions(-) create mode 100644 internal/policies/certificate/testdata/TestPolicyApply/golden/computer,_no_entries,_samba_cache_present diff --git a/internal/policies/certificate/cert-autoenroll b/internal/policies/certificate/cert-autoenroll index 03ec44731..4c2429a6d 100755 --- a/internal/policies/certificate/cert-autoenroll +++ b/internal/policies/certificate/cert-autoenroll @@ -86,6 +86,7 @@ def main(): ext.enroll(guid, entries, trust_dir, private_dir, global_trust_dir) else: ext.unenroll(guid) + os.removedirs(samba_cache_dir) def gpo_entries(entries_json): """ diff --git a/internal/policies/certificate/cert-autoenroll_test.go b/internal/policies/certificate/cert-autoenroll_test.go index 011f5d004..38fd4deb4 100644 --- a/internal/policies/certificate/cert-autoenroll_test.go +++ b/internal/policies/certificate/cert-autoenroll_test.go @@ -11,6 +11,7 @@ import ( "github.com/stretchr/testify/require" "github.com/ubuntu/adsys/internal/testutils" + "golang.org/x/exp/slices" ) const advancedConfigurationJSON = `[ @@ -137,6 +138,10 @@ func TestCertAutoenrollScript(t *testing.T) { got := strings.ReplaceAll(string(out), tmpdir, "#TMPDIR#") want := testutils.LoadWithUpdateFromGolden(t, got) require.Equal(t, want, got, "Unexpected output from cert-autoenroll script") + + if slices.Contains(tc.args, "unenroll") { + require.NoDirExists(t, filepath.Join(stateDir, "samba"), "Samba cache directory should have been removed on unenroll") + } }) } } diff --git a/internal/policies/certificate/certificate.go b/internal/policies/certificate/certificate.go index cc0c5858c..0ab985f62 100644 --- a/internal/policies/certificate/certificate.go +++ b/internal/policies/certificate/certificate.go @@ -7,10 +7,10 @@ // parse the relevant GPOs and delegate to an external Python script that will // request Samba to enroll or un-enroll the machine for certificates. // -// No action is taken if the certificate GPO is disabled, not configured, or -// absent. -// If the enroll flag is not set, the machine will be un-enrolled, -// namely the certificates will be removed and monitoring will stop. +// If the GPO is disabled/not configured, the policy manager will attempt to +// unenroll the machine only if traces of Samba cache are found on the disk. +// If the enroll flag is unchecked, the machine will be unenrolled, namely the +// certificates will be removed and monitoring will stop. // If any errors occur during the enrollment process, the manager will log them // prior to failing. package certificate @@ -145,19 +145,28 @@ func (m *Manager) ApplyPolicy(ctx context.Context, objectName string, isComputer m.mu.Lock() defer m.mu.Unlock() - idx := slices.IndexFunc(entries, func(e entry.Entry) bool { return e.Key == "autoenroll" }) - if idx == -1 { - log.Debug(ctx, "Certificate autoenrollment is not configured") - return nil - } - if !isComputer { log.Debug(ctx, "Certificate policy is only supported for computers, skipping...") return nil } if !isOnline { - log.Info(ctx, i18n.G("AD backend is offline, skipping certificate policy")) + log.Debug(ctx, i18n.G("AD backend is offline, skipping certificate policy")) + return nil + } + + idx := slices.IndexFunc(entries, func(e entry.Entry) bool { return e.Key == "autoenroll" }) + if idx == -1 { + // If the Samba cache directory doesn't exist, we don't have anything to unenroll + if _, err := os.Stat(m.sambaCacheDir); err != nil && os.IsNotExist(err) { + return nil + } + + log.Debug(ctx, "Certificate autoenrollment is not configured, unenrolling machine") + if err := m.runScript(ctx, "unenroll", objectName, "--samba_cache_dir", m.sambaCacheDir); err != nil { + return err + } + return nil } diff --git a/internal/policies/certificate/certificate_test.go b/internal/policies/certificate/certificate_test.go index 56fddffb5..d22c80752 100644 --- a/internal/policies/certificate/certificate_test.go +++ b/internal/policies/certificate/certificate_test.go @@ -43,15 +43,22 @@ func TestPolicyApply(t *testing.T) { autoenrollScriptError bool runScript bool + sambaDirExists bool wantErr bool }{ - "Computer, no entries": {}, + // No-op cases + "Computer, no entries": {}, + "Computer, autoenroll disabled": {entries: []entry.Entry{{Key: "autoenroll", Value: disabledValue}}}, + "Computer, domain is offline": {entries: []entry.Entry{enrollEntry}, isOffline: true}, + + // Enroll cases "Computer, configured to enroll": {entries: []entry.Entry{enrollEntry}, runScript: true}, "Computer, configured to enroll, advanced configuration": {entries: append(advancedConfigurationEntries, enrollEntry), runScript: true}, - "Computer, configured to unenroll": {entries: []entry.Entry{{Key: "autoenroll", Value: unenrollValue}}, runScript: true}, - "Computer, autoenroll disabled": {entries: []entry.Entry{{Key: "autoenroll", Value: disabledValue}}}, - "Computer, domain is offline": {entries: []entry.Entry{enrollEntry}, isOffline: true}, + + // Unenroll cases + "Computer, configured to unenroll": {entries: []entry.Entry{{Key: "autoenroll", Value: unenrollValue}}, runScript: true}, + "Computer, no entries, Samba cache present": {sambaDirExists: true, runScript: true}, "User, autoenroll not supported": {isUser: true, entries: []entry.Entry{enrollEntry}}, @@ -66,6 +73,11 @@ func TestPolicyApply(t *testing.T) { t.Parallel() tmpdir := t.TempDir() + sambaCacheDir := filepath.Join(tmpdir, "statedir", "samba") + if tc.sambaDirExists { + require.NoError(t, os.MkdirAll(sambaCacheDir, 0750), "Setup: Samba cache dir should be created") + } + autoenrollCmdOutputFile := filepath.Join(tmpdir, "autoenroll-output") autoenrollCmd := mockAutoenrollScript(t, autoenrollCmdOutputFile, tc.autoenrollScriptError) diff --git a/internal/policies/certificate/testdata/TestPolicyApply/golden/computer,_no_entries,_samba_cache_present b/internal/policies/certificate/testdata/TestPolicyApply/golden/computer,_no_entries,_samba_cache_present new file mode 100644 index 000000000..e877e31f7 --- /dev/null +++ b/internal/policies/certificate/testdata/TestPolicyApply/golden/computer,_no_entries,_samba_cache_present @@ -0,0 +1,3 @@ +unenroll keypress example.com --samba_cache_dir #TMPDIR#/statedir/samba +KRB5CCNAME=#TMPDIR#/rundir/krb5cc/keypress +PYTHONPATH=#TMPDIR#/sharedir/python From 7857337e99c32ded61576bf810ae8a77fbecc6d8 Mon Sep 17 00:00:00 2001 From: Gabriel Nagy Date: Fri, 28 Jul 2023 12:45:13 +0300 Subject: [PATCH 10/13] Mark certificate policy as Pro only and add tests --- .../ubuntu_pro_subscription_is_not_active | 1 + internal/policies/manager.go | 35 +++++++++++++++++-- internal/policies/manager_test.go | 18 ++++++---- .../cache/adsys/policies/hostname/policies | 4 +++ .../cache/adsys/policies/hostname/policies" | 4 +++ .../cache/adsys/policies/hostname/policies | 4 +++ .../cache/adsys/policies/hostname/policies | 4 +++ .../cache/adsys/policies/hostname/policies | 4 +++ .../cache/policies/all_entry_types/policies | 4 +++ .../policies/certificate_failing/policies | 8 +++++ 10 files changed, 77 insertions(+), 9 deletions(-) create mode 100644 internal/policies/testdata/cache/policies/certificate_failing/policies diff --git a/cmd/adsysd/integration_tests/testdata/TestServiceStatus/golden/ubuntu_pro_subscription_is_not_active b/cmd/adsysd/integration_tests/testdata/TestServiceStatus/golden/ubuntu_pro_subscription_is_not_active index e522b2cc7..747e1fae3 100644 --- a/cmd/adsysd/integration_tests/testdata/TestServiceStatus/golden/ubuntu_pro_subscription_is_not_active +++ b/cmd/adsysd/integration_tests/testdata/TestServiceStatus/golden/ubuntu_pro_subscription_is_not_active @@ -6,6 +6,7 @@ Next Refresh: Tue May 25 14:55 Ubuntu Pro subscription is not active on this machine. Rules belonging to the following policy types will not be applied: - apparmor + - certificate - mount - privilege - proxy diff --git a/internal/policies/manager.go b/internal/policies/manager.go index 7f1227374..953ef5f75 100644 --- a/internal/policies/manager.go +++ b/internal/policies/manager.go @@ -54,7 +54,7 @@ import ( // ProOnlyRules are the rules that are only available for Pro subscribers. They // will be filtered otherwise. -var ProOnlyRules = []string{"privilege", "scripts", "mount", "apparmor", "proxy"} +var ProOnlyRules = []string{"privilege", "scripts", "mount", "apparmor", "proxy", "certificate"} // Manager handles all managers for various policy handlers. type Manager struct { @@ -107,6 +107,7 @@ type options struct { gdm *gdm.Manager apparmorParserCmd []string + certAutoenrollCmd []string } // Option reprents an optional function to change Policies behavior. @@ -120,6 +121,14 @@ func WithCacheDir(p string) Option { } } +// WithStateDir specifies a personalized state directory. +func WithStateDir(p string) Option { + return func(o *options) error { + o.stateDir = p + return nil + } +} + // WithDconfDir specifies a personalized dconf directory. func WithDconfDir(p string) Option { return func(o *options) error { @@ -152,6 +161,14 @@ func WithRunDir(p string) Option { } } +// WithShareDir specifies a personalized share directory. +func WithShareDir(p string) Option { + return func(o *options) error { + o.shareDir = p + return nil + } +} + // WithApparmorDir specifies a personalized apparmor directory. func WithApparmorDir(p string) Option { return func(o *options) error { @@ -201,6 +218,14 @@ func WithSystemdCaller(p systemdCaller) Option { } } +// WithCertAutoenrollCmd specifies a personalized certificate autoenroll command. +func WithCertAutoenrollCmd(cmd []string) Option { + return func(o *options) error { + o.certAutoenrollCmd = cmd + return nil + } +} + // NewManager returns a new manager with all default policy handlers. func NewManager(bus *dbus.Conn, hostname string, backend backends.Backend, opts ...Option) (m *Manager, err error) { defer decorate.OnError(&err, i18n.G("can't create a new policy handlers manager")) @@ -266,11 +291,15 @@ func NewManager(bus *dbus.Conn, hostname string, backend backends.Backend, opts proxyManager := proxy.New(bus, proxyOptions...) // certificate manager - certificateManager := certificate.New(backend.Domain(), + certificateOpts := []certificate.Option{ certificate.WithStateDir(args.stateDir), certificate.WithRunDir(args.runDir), certificate.WithShareDir(args.shareDir), - ) + } + if args.certAutoenrollCmd != nil { + certificateOpts = append(certificateOpts, certificate.WithCertAutoenrollCmd(args.certAutoenrollCmd)) + } + certificateManager := certificate.New(backend.Domain(), certificateOpts...) // inject applied dconf mangager if we need to build a gdm manager if args.gdm == nil { diff --git a/internal/policies/manager_test.go b/internal/policies/manager_test.go index ebd8a323e..370589c9c 100644 --- a/internal/policies/manager_test.go +++ b/internal/policies/manager_test.go @@ -56,12 +56,13 @@ func TestApplyPolicies(t *testing.T) { "Second call with no subscription don't remove scripts if session hasn’t ended": {policiesDir: "all_entry_types", secondCallWithNoSubscription: true, scriptSessionEndedForSecondCall: false}, // Error cases - "Error when applying dconf policy": {policiesDir: "dconf_failing", wantErr: true}, - "Error when applying privilege policy": {makeDirReadOnly: "etc/sudoers.d", policiesDir: "all_entry_types", wantErr: true}, - "Error when applying scripts policy": {makeDirReadOnly: "run/adsys/machine", policiesDir: "all_entry_types", wantErr: true}, - "Error when applying apparmor policy": {makeDirReadOnly: "etc/apparmor.d/adsys", policiesDir: "all_entry_types", wantErr: true}, - "Error when applying mount policy": {makeDirReadOnly: "etc/systemd/system", policiesDir: "all_entry_types", wantErr: true}, - "Error when applying proxy policy": {noUbuntuProxyManager: true, policiesDir: "all_entry_types", wantErr: true}, + "Error when applying dconf policy": {policiesDir: "dconf_failing", wantErr: true}, + "Error when applying privilege policy": {makeDirReadOnly: "etc/sudoers.d", policiesDir: "all_entry_types", wantErr: true}, + "Error when applying scripts policy": {makeDirReadOnly: "run/adsys/machine", policiesDir: "all_entry_types", wantErr: true}, + "Error when applying apparmor policy": {makeDirReadOnly: "etc/apparmor.d/adsys", policiesDir: "all_entry_types", wantErr: true}, + "Error when applying mount policy": {makeDirReadOnly: "etc/systemd/system", policiesDir: "all_entry_types", wantErr: true}, + "Error when applying proxy policy": {noUbuntuProxyManager: true, policiesDir: "all_entry_types", wantErr: true}, + "Error when applying certificate policy": {policiesDir: "certificate_failing", wantErr: true}, } for name, tc := range tests { tc := tc @@ -82,6 +83,8 @@ func TestApplyPolicies(t *testing.T) { sudoersDir := filepath.Join(fakeRootDir, "etc", "sudoers.d") apparmorDir := filepath.Join(fakeRootDir, "etc", "apparmor.d", "adsys") systemUnitDir := filepath.Join(fakeRootDir, "etc", "systemd", "system") + stateDir := filepath.Join(fakeRootDir, "var", "lib", "adsys") + shareDir := filepath.Join(fakeRootDir, "usr", "share", "adsys") loadedPoliciesFile := filepath.Join(fakeRootDir, "sys", "kernel", "security", "apparmor", "profiles") err = os.MkdirAll(filepath.Dir(loadedPoliciesFile), 0700) @@ -102,13 +105,16 @@ func TestApplyPolicies(t *testing.T) { hostname, mockBackend{}, policies.WithCacheDir(cacheDir), + policies.WithStateDir(stateDir), policies.WithRunDir(runDir), + policies.WithShareDir(shareDir), policies.WithDconfDir(dconfDir), policies.WithPolicyKitDir(policyKitDir), policies.WithSudoersDir(sudoersDir), policies.WithApparmorDir(apparmorDir), policies.WithApparmorFsDir(filepath.Dir(loadedPoliciesFile)), policies.WithApparmorParserCmd([]string{"/bin/true"}), + policies.WithCertAutoenrollCmd([]string{"/bin/true"}), policies.WithSystemUnitDir(systemUnitDir), policies.WithProxyApplier(&mockProxyApplier{wantApplyError: tc.noUbuntuProxyManager}), policies.WithSystemdCaller(&testutils.MockSystemdCaller{}), diff --git a/internal/policies/testdata/TestApplyPolicies/golden/no_subscription_is_only_dconf_content/var/cache/adsys/policies/hostname/policies b/internal/policies/testdata/TestApplyPolicies/golden/no_subscription_is_only_dconf_content/var/cache/adsys/policies/hostname/policies index 65c86435b..0fa3a5d6a 100644 --- a/internal/policies/testdata/TestApplyPolicies/golden/no_subscription_is_only_dconf_content/var/cache/adsys/policies/hostname/policies +++ b/internal/policies/testdata/TestApplyPolicies/golden/no_subscription_is_only_dconf_content/var/cache/adsys/policies/hostname/policies @@ -9,6 +9,10 @@ gpos: usr.bin.bar nested/usr.bin.baz disabled: false + certificate: + - key: autoenroll + value: "7" + disabled: false dconf: - key: path/to/key1 value: ValueOfKey1 diff --git "a/internal/policies/testdata/TestApplyPolicies/golden/second_call_with_no_subscription_don't_remove_scripts_if_session_hasn\342\200\231t_ended/var/cache/adsys/policies/hostname/policies" "b/internal/policies/testdata/TestApplyPolicies/golden/second_call_with_no_subscription_don't_remove_scripts_if_session_hasn\342\200\231t_ended/var/cache/adsys/policies/hostname/policies" index 65c86435b..0fa3a5d6a 100644 --- "a/internal/policies/testdata/TestApplyPolicies/golden/second_call_with_no_subscription_don't_remove_scripts_if_session_hasn\342\200\231t_ended/var/cache/adsys/policies/hostname/policies" +++ "b/internal/policies/testdata/TestApplyPolicies/golden/second_call_with_no_subscription_don't_remove_scripts_if_session_hasn\342\200\231t_ended/var/cache/adsys/policies/hostname/policies" @@ -9,6 +9,10 @@ gpos: usr.bin.bar nested/usr.bin.baz disabled: false + certificate: + - key: autoenroll + value: "7" + disabled: false dconf: - key: path/to/key1 value: ValueOfKey1 diff --git a/internal/policies/testdata/TestApplyPolicies/golden/second_call_with_no_subscription_should_remove_everything_but_dconf_content/var/cache/adsys/policies/hostname/policies b/internal/policies/testdata/TestApplyPolicies/golden/second_call_with_no_subscription_should_remove_everything_but_dconf_content/var/cache/adsys/policies/hostname/policies index 65c86435b..0fa3a5d6a 100644 --- a/internal/policies/testdata/TestApplyPolicies/golden/second_call_with_no_subscription_should_remove_everything_but_dconf_content/var/cache/adsys/policies/hostname/policies +++ b/internal/policies/testdata/TestApplyPolicies/golden/second_call_with_no_subscription_should_remove_everything_but_dconf_content/var/cache/adsys/policies/hostname/policies @@ -9,6 +9,10 @@ gpos: usr.bin.bar nested/usr.bin.baz disabled: false + certificate: + - key: autoenroll + value: "7" + disabled: false dconf: - key: path/to/key1 value: ValueOfKey1 diff --git a/internal/policies/testdata/TestApplyPolicies/golden/succeed/var/cache/adsys/policies/hostname/policies b/internal/policies/testdata/TestApplyPolicies/golden/succeed/var/cache/adsys/policies/hostname/policies index 65c86435b..0fa3a5d6a 100644 --- a/internal/policies/testdata/TestApplyPolicies/golden/succeed/var/cache/adsys/policies/hostname/policies +++ b/internal/policies/testdata/TestApplyPolicies/golden/succeed/var/cache/adsys/policies/hostname/policies @@ -9,6 +9,10 @@ gpos: usr.bin.bar nested/usr.bin.baz disabled: false + certificate: + - key: autoenroll + value: "7" + disabled: false dconf: - key: path/to/key1 value: ValueOfKey1 diff --git a/internal/policies/testdata/TestApplyPolicies/golden/succeed_if_checking_for_backend_online_status_returns_an_error/var/cache/adsys/policies/hostname/policies b/internal/policies/testdata/TestApplyPolicies/golden/succeed_if_checking_for_backend_online_status_returns_an_error/var/cache/adsys/policies/hostname/policies index 65c86435b..0fa3a5d6a 100644 --- a/internal/policies/testdata/TestApplyPolicies/golden/succeed_if_checking_for_backend_online_status_returns_an_error/var/cache/adsys/policies/hostname/policies +++ b/internal/policies/testdata/TestApplyPolicies/golden/succeed_if_checking_for_backend_online_status_returns_an_error/var/cache/adsys/policies/hostname/policies @@ -9,6 +9,10 @@ gpos: usr.bin.bar nested/usr.bin.baz disabled: false + certificate: + - key: autoenroll + value: "7" + disabled: false dconf: - key: path/to/key1 value: ValueOfKey1 diff --git a/internal/policies/testdata/cache/policies/all_entry_types/policies b/internal/policies/testdata/cache/policies/all_entry_types/policies index 61abf0957..a25406b76 100644 --- a/internal/policies/testdata/cache/policies/all_entry_types/policies +++ b/internal/policies/testdata/cache/policies/all_entry_types/policies @@ -56,3 +56,7 @@ gpos: disabled: true - key: proxy/no-proxy value: localhost,127.0.0.1,::1 + certificate: + - key: autoenroll + value: "7" + disabled: false diff --git a/internal/policies/testdata/cache/policies/certificate_failing/policies b/internal/policies/testdata/cache/policies/certificate_failing/policies new file mode 100644 index 000000000..a51413132 --- /dev/null +++ b/internal/policies/testdata/cache/policies/certificate_failing/policies @@ -0,0 +1,8 @@ +gpos: +- id: '{GPOId}' + name: GPOName + rules: + certificate: + - key: autoenroll + value: "NotANumber" + disabled: false From 3eb2769c1899fd2d6ff00030f85bcd9ab27097ca Mon Sep 17 00:00:00 2001 From: Gabriel Nagy Date: Fri, 28 Jul 2023 12:55:56 +0300 Subject: [PATCH 11/13] Disable parallel testing for autoenroll script Because Python coverage can't be parallelized, avoid running these tests in parallel. --- internal/policies/certificate/cert-autoenroll_test.go | 4 ---- 1 file changed, 4 deletions(-) diff --git a/internal/policies/certificate/cert-autoenroll_test.go b/internal/policies/certificate/cert-autoenroll_test.go index 38fd4deb4..8f7bbc94a 100644 --- a/internal/policies/certificate/cert-autoenroll_test.go +++ b/internal/policies/certificate/cert-autoenroll_test.go @@ -60,8 +60,6 @@ const advancedConfigurationJSON = `[ ]` func TestCertAutoenrollScript(t *testing.T) { - t.Parallel() - coverageOn := testutils.PythonCoverageToGoFormat(t, "cert-autoenroll", false) certAutoenrollCmd := "./cert-autoenroll" if coverageOn { @@ -108,8 +106,6 @@ func TestCertAutoenrollScript(t *testing.T) { tc := tc name := name t.Run(name, func(t *testing.T) { - t.Parallel() - tmpdir := t.TempDir() stateDir := filepath.Join(tmpdir, "state") privateDir := filepath.Join(tmpdir, "private") From 9b0f22c701465616b9ff4063f32f58694300a16e Mon Sep 17 00:00:00 2001 From: Gabriel Nagy Date: Fri, 28 Jul 2023 17:01:14 +0300 Subject: [PATCH 12/13] Check error when typecasting GPO data --- internal/policies/certificate/certificate.go | 13 ++++++++----- internal/policies/certificate/certificate_test.go | 5 +++++ 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/internal/policies/certificate/certificate.go b/internal/policies/certificate/certificate.go index 0ab985f62..27241509e 100644 --- a/internal/policies/certificate/certificate.go +++ b/internal/policies/certificate/certificate.go @@ -194,7 +194,11 @@ func (m *Manager) ApplyPolicy(ctx context.Context, objectName string, isComputer keyparts := strings.Split(entry.Key, "/") keyname := strings.Join(keyparts[:len(keyparts)-1], `\`) valuename := keyparts[len(keyparts)-1] - polSrvRegistryEntries = append(polSrvRegistryEntries, gpoEntry{keyname, valuename, gpoData(entry.Value, valuename), gpoType(valuename)}) + gpoData, err := gpoData(entry.Value, valuename) + if err != nil { + return fmt.Errorf(i18n.G("failed to parse policy entry value: %w"), err) + } + polSrvRegistryEntries = append(polSrvRegistryEntries, gpoEntry{keyname, valuename, gpoData, gpoType(valuename)}) log.Debugf(ctx, "Certificate policy entry: %#v", entry) } @@ -249,13 +253,12 @@ func (m *Manager) runScript(ctx context.Context, action, objectName string, extr } // gpoData returns the data for a GPO entry. -func gpoData(data, value string) any { +func gpoData(data, value string) (any, error) { if slices.Contains(integerGPOValues, value) { - intData, _ := strconv.Atoi(data) - return intData + return strconv.Atoi(data) } - return data + return data, nil } // gpoType returns the type for a GPO entry. diff --git a/internal/policies/certificate/certificate_test.go b/internal/policies/certificate/certificate_test.go index d22c80752..872a3efd2 100644 --- a/internal/policies/certificate/certificate_test.go +++ b/internal/policies/certificate/certificate_test.go @@ -65,6 +65,11 @@ func TestPolicyApply(t *testing.T) { // Error cases "Error on autoenroll script failure": {autoenrollScriptError: true, entries: []entry.Entry{enrollEntry}, wantErr: true}, "Error on invalid autoenroll value": {entries: []entry.Entry{{Key: "autoenroll", Value: "notanumber"}}, wantErr: true}, + "Error on invalid advanced configuration value": { + entries: []entry.Entry{ + enrollEntry, + {Key: "Software/Policies/Microsoft/Cryptography/PolicyServers/37c9dc30f207f27f61a2f7c3aed598a6e2920b54/Flags", Value: "NotANumber"}, + }, wantErr: true}, } for name, tc := range tests { From 1a7be22ccfcfc71ddeddd96162cbb7c8bbae189a Mon Sep 17 00:00:00 2001 From: Gabriel Nagy Date: Mon, 31 Jul 2023 12:00:30 +0300 Subject: [PATCH 13/13] Add GPO parsing test for certificate policy --- internal/ad/ad_test.go | 18 ++++++++++++++++++ .../GPT.INI | 3 +++ .../Machine/Registry.pol | Bin 0 -> 4406 bytes 3 files changed, 21 insertions(+) create mode 100644 internal/ad/testdata/AD/SYSVOL/gpoonly.com/Policies/filtered-with-certificate-autoenrollment/GPT.INI create mode 100644 internal/ad/testdata/AD/SYSVOL/gpoonly.com/Policies/filtered-with-certificate-autoenrollment/Machine/Registry.pol diff --git a/internal/ad/ad_test.go b/internal/ad/ad_test.go index fb34f3379..2d0a07a6b 100644 --- a/internal/ad/ad_test.go +++ b/internal/ad/ad_test.go @@ -396,6 +396,24 @@ func TestGetPolicies(t *testing.T) { }}}, }}, }, + "Include non Ubuntu keys used to configure certificate autoenrollment": { + objectName: hostname, + objectClass: ad.ComputerObject, + gpoListArgs: []string{"gpoonly.com", hostname + ":filtered-with-certificate-autoenrollment"}, + want: policies.Policies{GPOs: []policies.GPO{ + {ID: "filtered-with-certificate-autoenrollment", Name: "filtered-with-certificate-autoenrollment-name", Rules: map[string][]entry.Entry{ + "certificate": { + {Key: "autoenroll", Value: "1"}, + {Key: "Software/Policies/Microsoft/Cryptography/PolicyServers/Flags", Value: "0"}, + {Key: "Software/Policies/Microsoft/Cryptography/PolicyServers/37c9dc30f207f27f61a2f7c3aed598a6e2920b54/URL", Value: "LDAP:"}, + {Key: "Software/Policies/Microsoft/Cryptography/PolicyServers/37c9dc30f207f27f61a2f7c3aed598a6e2920b54/PolicyID", Value: "{A5E9BF57-71C6-443A-B7FC-79EFA6F73EBD}"}, + {Key: "Software/Policies/Microsoft/Cryptography/PolicyServers/37c9dc30f207f27f61a2f7c3aed598a6e2920b54/FriendlyName", Value: "Active Directory Enrollment Policy"}, + {Key: "Software/Policies/Microsoft/Cryptography/PolicyServers/37c9dc30f207f27f61a2f7c3aed598a6e2920b54/Flags", Value: "20"}, + {Key: "Software/Policies/Microsoft/Cryptography/PolicyServers/37c9dc30f207f27f61a2f7c3aed598a6e2920b54/AuthFlags", Value: "2"}, + {Key: "Software/Policies/Microsoft/Cryptography/PolicyServers/37c9dc30f207f27f61a2f7c3aed598a6e2920b54/Cost", Value: "2147483645"}, + }}}, + }}, + }, "Ignore errors on non Ubuntu keys": { gpoListArgs: []string{"gpoonly.com", "bob:unsupported-with-errors"}, want: policies.Policies{GPOs: []policies.GPO{ diff --git a/internal/ad/testdata/AD/SYSVOL/gpoonly.com/Policies/filtered-with-certificate-autoenrollment/GPT.INI b/internal/ad/testdata/AD/SYSVOL/gpoonly.com/Policies/filtered-with-certificate-autoenrollment/GPT.INI new file mode 100644 index 000000000..c5498a917 --- /dev/null +++ b/internal/ad/testdata/AD/SYSVOL/gpoonly.com/Policies/filtered-with-certificate-autoenrollment/GPT.INI @@ -0,0 +1,3 @@ +[General] +Version=1000 +displayName=New Group Policy Object diff --git a/internal/ad/testdata/AD/SYSVOL/gpoonly.com/Policies/filtered-with-certificate-autoenrollment/Machine/Registry.pol b/internal/ad/testdata/AD/SYSVOL/gpoonly.com/Policies/filtered-with-certificate-autoenrollment/Machine/Registry.pol new file mode 100644 index 0000000000000000000000000000000000000000..94c6f4bb93ae304863b41294f9c7ae3bbbefb403 GIT binary patch literal 4406 zcmds)PjAye5XBz|aX=jT06imBf~JiLIhp)Hh_*xt91!6Ug40$?W7LE~gb<$`mEVjT zc?&fca2-X~t|zm*@|$_HGm~CFIl3k3od!xZ(L!G|(wPzs)k8DYiN=gXb2JD3>kR$; za`#Xzb}#jblc|o7N65!$q^2J7v^hIvEEq2tOU~MweYL5X&;R?|A@(JfCEz;u*}nb0 z1Z#(i_1S&U!6x(2l;@Oh&d{2ru{wcAGymN2m25VDRDPWjhHFj*X8nNE;)du^!XzR=o>m63x`d*PmUE*j&9>}~LMhSpQitl0{ z5AjDt{%YtWb4`zk^Bv~M-!|9r?g-t)_tlUS_U>`w338oZJIHqC1KM3@>5cl@cPZoU zzRUY~J?5r7-55JVzrMh_JZo=4w3ZVaUGkJ2)ms7ukAyV{C2FGt4i$p0EP^ zytw%BFA+QN-qHHVQ)-{}basO#j4`>|^wy)sto7;ntTVlSkKM&=