├── AUTHORS ├── NEWS ├── ChangeLog ├── src ├── fleetcommanderclient │ ├── __init__.py │ ├── adapters │ │ ├── __init__.py │ │ ├── firefoxbookmarks.py │ │ ├── firefox.py │ │ ├── base.py │ │ ├── chromium.py │ │ ├── goa.py │ │ ├── nm.py │ │ └── dconf.py │ ├── configadapters │ │ ├── __init__.py │ │ ├── base.py │ │ ├── chromium.py │ │ ├── firefox.py │ │ ├── firefoxbookmarks.py │ │ ├── goa.py │ │ ├── networkmanager.py │ │ └── dconf.py │ ├── configloader.py │ ├── settingscompiler.py │ ├── mergers.py │ ├── fcclientad.py │ └── fcclient.py └── Makefile.am ├── tests ├── azure │ ├── templates │ │ ├── variables.yml │ │ ├── variables-common.yml │ │ ├── build-fedora.yml │ │ ├── configure-fedora.yml │ │ ├── publish-build.yml │ │ ├── prepare-lint-fedora.yml │ │ ├── variables-fedora.yml │ │ └── prepare-build-fedora.yml │ └── azure-pipelines.yml ├── data │ ├── test_config_file.conf │ ├── sampleprofiledata │ │ ├── 0070-0070-0000-0000-0000-Invalid.profile │ │ ├── 0060-0060-0000-0000-0000-Test2.profile │ │ ├── 0050-0050-0000-0000-0000-Test1.profile │ │ └── 0090-0090-0000-0000-0000-Test3.profile │ ├── dconf_profile_compiled.dat │ └── test_profile.json ├── tools │ └── dconf ├── Makefile.am ├── 17_fcclientad.sh ├── 09_fcclient.sh ├── _10_mmock_realmd_dbus.py ├── test_fcclient_service.py ├── 00_configloader.py ├── test_fcclientad_service.py ├── smbmock.py ├── 06_configadapter_chromium.py ├── 03_configadapter_goa.py ├── 11_adapter_chromium.py ├── 13_adapter_goa.py ├── 16_adapter_firefoxbookmarks.py ├── 08_configadapter_firefoxbookmarks.py ├── 05_configadapter_dconf.py ├── 07_configadapter_firefox.py ├── _fcclientad_tests.py ├── 12_adapter_firefox.py ├── 14_adapter_dconf.py ├── _fcclient_tests.py ├── ldapmock.py └── 04_configadapter_nm.py ├── data ├── fleet-commander-client.conf ├── org.freedesktop.FleetCommanderClient.service.in ├── org.freedesktop.FleetCommanderClientAD.service.in ├── fleet-commander-adretriever.service.in ├── fleet-commander-client.service.in ├── fleet-commander-clientad.service.in ├── org.freedesktop.FleetCommanderClient.conf ├── org.freedesktop.FleetCommanderClientAD.conf └── Makefile.am ├── autogen.sh ├── pylint_plugins.py ├── .gitignore ├── README ├── COPYING.MIT ├── pylintrc ├── COPYING.BSD3 ├── m4 └── as-ac-expand.m4 ├── Makefile.am ├── configure.ac └── fleet-commander-client.spec /AUTHORS: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /NEWS: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ChangeLog: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/fleetcommanderclient/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/azure/templates/variables.yml: -------------------------------------------------------------------------------- 1 | variables-fedora.yml -------------------------------------------------------------------------------- /data/fleet-commander-client.conf: -------------------------------------------------------------------------------- 1 | [fleet-commander] 2 | goa_run_path = /run/goa-1.0 3 | -------------------------------------------------------------------------------- /tests/azure/templates/variables-common.yml: -------------------------------------------------------------------------------- 1 | variables: 2 | VM_IMAGE: 'Ubuntu-18.04' 3 | -------------------------------------------------------------------------------- /tests/data/test_config_file.conf: -------------------------------------------------------------------------------- 1 | [fleet-commander] 2 | goa_run_path = /run/goa-1.0 3 | -------------------------------------------------------------------------------- /tests/tools/dconf: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | if [ $1 == 'compile' ]; then 3 | echo "COMPILED" > $2 4 | fi 5 | -------------------------------------------------------------------------------- /tests/data/sampleprofiledata/0070-0070-0000-0000-0000-Invalid.profile: -------------------------------------------------------------------------------- 1 | FOO 2 | BAR 3 | BAZ 4 | FOOBAR 5 | BAZINGA 6 | -------------------------------------------------------------------------------- /tests/data/dconf_profile_compiled.dat: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fleet-commander/fc-client/HEAD/tests/data/dconf_profile_compiled.dat -------------------------------------------------------------------------------- /autogen.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | git submodule update --init --recursive 3 | aclocal \ 4 | && automake --gnu -a -c \ 5 | && autoconf 6 | ./configure $@ 7 | -------------------------------------------------------------------------------- /tests/azure/templates/build-fedora.yml: -------------------------------------------------------------------------------- 1 | steps: 2 | - script: | 3 | set -e 4 | echo "Running make target 'rpms'" 5 | make rpms 6 | displayName: Build RPM packages 7 | -------------------------------------------------------------------------------- /tests/azure/templates/configure-fedora.yml: -------------------------------------------------------------------------------- 1 | steps: 2 | - script: | 3 | set -e 4 | printf "Configuring project" 5 | git submodule update --init --recursive 6 | autoreconf -ifv 7 | ./configure 8 | displayName: Configure the project 9 | -------------------------------------------------------------------------------- /tests/azure/templates/publish-build.yml: -------------------------------------------------------------------------------- 1 | parameters: 2 | artifactName: '' 3 | targetPath: '' 4 | displayName: '' 5 | 6 | steps: 7 | - task: PublishPipelineArtifact@1 8 | inputs: 9 | artifactName: ${{ parameters.artifactName }} 10 | targetPath: ${{ parameters.targetPath }} 11 | displayName: ${{ parameters.displayName }} 12 | -------------------------------------------------------------------------------- /data/org.freedesktop.FleetCommanderClient.service.in: -------------------------------------------------------------------------------- 1 | # Fleet Commander Client DBus service activation config 2 | [D-BUS Service] 3 | Name=org.freedesktop.FleetCommanderClient 4 | Environment=PYTHONPATH=@FCPYTHONDIR@ 5 | Exec=@PYTHON@ -m fleetcommanderclient.fcclient --configuration @XDGCONFIGDIR@/fleet-commander-client.conf 6 | User=root 7 | SystemdService=fleet-commander-client.service 8 | -------------------------------------------------------------------------------- /data/org.freedesktop.FleetCommanderClientAD.service.in: -------------------------------------------------------------------------------- 1 | # Fleet Commander Client AD DBus service activation config 2 | [D-BUS Service] 3 | Name=org.freedesktop.FleetCommanderClientAD 4 | Environment=PYTHONPATH=@FCPYTHONDIR@ 5 | Exec=@PYTHON@ -m fleetcommanderclient.fcclientad --configuration @XDGCONFIGDIR@/fleet-commander-client.conf 6 | User=root 7 | SystemdService=fleet-commander-clientad.service 8 | -------------------------------------------------------------------------------- /tests/azure/templates/prepare-lint-fedora.yml: -------------------------------------------------------------------------------- 1 | steps: 2 | - script: | 3 | set -e 4 | printf "Installing pip module\n" 5 | sudo dnf install -y \ 6 | python3-pip \ 7 | 8 | printf "Installing latest Python lint dependencies\n" 9 | pip install \ 10 | --user --force \ 11 | pylint \ 12 | black \ 13 | displayName: Install latest Python lint dependencies 14 | -------------------------------------------------------------------------------- /data/fleet-commander-adretriever.service.in: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Fleet Commander Client Active Directory service 3 | 4 | [Service] 5 | Type=simple 6 | Environment=PYTHONPATH=@FCPYTHONDIR@ 7 | ExecStart=@PYTHON@ -m fleetcommanderclient.fcadretriever --configuration @XDGCONFIGDIR@/fleet-commander-client.conf 8 | StandardOutput=syslog 9 | StandardError=inherit 10 | 11 | [Install] 12 | WantedBy=default.target 13 | -------------------------------------------------------------------------------- /data/fleet-commander-client.service.in: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Fleet Commander Client dbus service 3 | 4 | [Service] 5 | Type=dbus 6 | BusName=org.freedesktop.FleetCommanderClient 7 | Environment=PYTHONPATH=@FCPYTHONDIR@ 8 | ExecStart=@PYTHON@ -m fleetcommanderclient.fcclient --configuration @XDGCONFIGDIR@/fleet-commander-client.conf 9 | StandardOutput=syslog 10 | StandardError=inherit 11 | 12 | [Install] 13 | WantedBy=multi-user.target 14 | -------------------------------------------------------------------------------- /data/fleet-commander-clientad.service.in: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Fleet Commander Client AD dbus service 3 | 4 | [Service] 5 | Type=dbus 6 | BusName=org.freedesktop.FleetCommanderClientAD 7 | Environment=PYTHONPATH=@FCPYTHONDIR@ 8 | ExecStart=@PYTHON@ -m fleetcommanderclient.fcclientad --configuration @XDGCONFIGDIR@/fleet-commander-client.conf 9 | StandardOutput=syslog 10 | StandardError=inherit 11 | 12 | [Install] 13 | WantedBy=multi-user.target 14 | -------------------------------------------------------------------------------- /pylint_plugins.py: -------------------------------------------------------------------------------- 1 | """ Plugin to teach Pylint about FreeIPA API 2 | """ 3 | 4 | import textwrap 5 | 6 | from astroid import MANAGER 7 | from astroid.builder import AstroidBuilder 8 | 9 | 10 | def register(linter): 11 | pass 12 | 13 | 14 | AstroidBuilder(MANAGER).string_build( 15 | textwrap.dedent( 16 | """ 17 | from ipalib import api 18 | from ipalib import plugable 19 | 20 | api.Backend = plugable.APINameSpace(api, None) 21 | api.Command = plugable.APINameSpace(api, None) 22 | """ 23 | ) 24 | ) 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | rpmbuild/ 3 | Makefile 4 | Makefile.in 5 | */Makefile 6 | */Makefile.in 7 | .*.swp 8 | */.*.swp 9 | admin/profiles/*.json 10 | admin/fleetcommander/constants.py 11 | data/*.service 12 | INSTALL 13 | aclocal.m4 14 | admin/demoscripts/ 15 | autom4te.cache/ 16 | config.log 17 | config.status 18 | configure 19 | install-sh 20 | missing 21 | fleet-commander*.tar.[gx]z 22 | tests/*.trs 23 | tests/*.log 24 | *.py[co] 25 | test-driver 26 | build-rpm.sh 27 | install-rpm.sh 28 | full-install-rpm.sh 29 | check-full.sh 30 | check.sh 31 | .vscode 32 | *.code-workspace 33 | build-copr.sh 34 | dbus-call-method.sh 35 | dbus-send-test.sh 36 | install-ipaclient.sh 37 | test-dbus-auth.py 38 | -------------------------------------------------------------------------------- /tests/azure/templates/variables-fedora.yml: -------------------------------------------------------------------------------- 1 | variables: 2 | FC_PLATFORM: fedora 3 | # the Docker public image to build FC packages (rpms) 4 | DOCKER_BUILD_IMAGE: 'fedora:32' 5 | 6 | # the template to install FC buildtime dependencies 7 | PREPARE_BUILD_TEMPLATE: ${{ format('prepare-build-{0}.yml', variables.FC_PLATFORM) }} 8 | 9 | # the template to configure project (rpms) 10 | CONFIGURE_TEMPLATE: ${{ format('configure-{0}.yml', variables.FC_PLATFORM) }} 11 | 12 | # the template to build FC packages (rpms) 13 | BUILD_TEMPLATE: ${{ format('build-{0}.yml', variables.FC_PLATFORM) }} 14 | 15 | # the template to install latest Pylint 16 | PREPARE_LINT_TEMPLATE: ${{ format('prepare-lint-{0}.yml', variables.FC_PLATFORM) }} 17 | -------------------------------------------------------------------------------- /data/org.freedesktop.FleetCommanderClient.conf: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /tests/azure/templates/prepare-build-fedora.yml: -------------------------------------------------------------------------------- 1 | steps: 2 | - script: | 3 | set -e 4 | sudo rm -rf /var/cache/dnf/* 5 | sudo dnf makecache || : 6 | printf "Installing base dev dependencies\n" 7 | sudo dnf install -y \ 8 | 'dnf-command(builddep)' \ 9 | autoconf \ 10 | autoconf-archive \ 11 | automake \ 12 | gettext-devel \ 13 | make \ 14 | rpm-build \ 15 | 16 | printf "Installing FC dev dependencies\n" 17 | sudo dnf builddep -y \ 18 | --skip-broken \ 19 | -D "with_check 1" \ 20 | --spec fleet-commander-client.spec \ 21 | --best \ 22 | --allowerasing \ 23 | --setopt=install_weak_deps=False \ 24 | 25 | displayName: Prepare build environment 26 | -------------------------------------------------------------------------------- /data/org.freedesktop.FleetCommanderClientAD.conf: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /README: -------------------------------------------------------------------------------- 1 | Fleet Commander 2 | 3 | Fleet Commander is an application that allows you to manage the desktop 4 | configuration of a large network of users and workstations/laptops. 5 | 6 | It is primarily targeted to Linux systems based on the GNOME desktop. 7 | 8 | Fleet Commander consists on two components: 9 | 10 | - a admin interface that stores profiles in your directory server (AKA FreeIPA, Active Directory) 11 | - and a client side service that runs on every host of the network when you log in. 12 | 13 | Fleet Commander relies on libvirt and KVM to generate the profile data 14 | dinamically from a template VM running the same environment as the rest of the 15 | network. 16 | 17 | SETUP 18 | 19 | These are the instructions to build and install the client daemon: 20 | 21 | $ ./configure --prefix=$PREFIX # where $PREFIX can be /usr or /usr/local 22 | $ make 23 | $ make install 24 | 25 | -------------------------------------------------------------------------------- /tests/Makefile.am: -------------------------------------------------------------------------------- 1 | TESTS_ENVIRONMENT = export PATH=$(abs_top_srcdir)/tests/tools:$(abs_top_srcdir)/tests:$(PATH); export TOPSRCDIR=$(abs_top_srcdir); export PYTHON=@PYTHON@; export FC_TESTING=true; 2 | TESTS = 00_configloader.py 01_mergers.py 02_settingscompiler.py 03_configadapter_goa.py 04_configadapter_nm.py 05_configadapter_dconf.py 06_configadapter_chromium.py 07_configadapter_firefox.py 08_configadapter_firefoxbookmarks.py 09_fcclient.sh 10_fcadretriever.py 11_adapter_chromium.py 12_adapter_firefox.py 13_adapter_goa.py 14_adapter_dconf.py 15_adapter_nm.py 16_adapter_firefoxbookmarks.py 17_fcclientad.sh 3 | 4 | EXTRA_DIST = \ 5 | $(TESTS) \ 6 | _fcclient_tests.py \ 7 | _fcclientad_tests.py \ 8 | test_fcclient_service.py \ 9 | test_fcclientad_service.py \ 10 | ldapmock.py \ 11 | smbmock.py \ 12 | data/test_config_file.conf \ 13 | data/sampleprofiledata/0050-0050-0000-0000-0000-Test1.profile \ 14 | data/sampleprofiledata/0060-0060-0000-0000-0000-Test2.profile \ 15 | data/sampleprofiledata/0070-0070-0000-0000-0000-Invalid.profile \ 16 | data/sampleprofiledata/0090-0090-0000-0000-0000-Test3.profile \ 17 | tools/dconf 18 | -------------------------------------------------------------------------------- /tests/data/sampleprofiledata/0060-0060-0000-0000-0000-Test2.profile: -------------------------------------------------------------------------------- 1 | { 2 | "org.gnome.gsettings": [ 3 | { 4 | "signature": "s", 5 | "value": "'#CCCCCC'", 6 | "key": "/org/yorba/shotwell/preferences/ui/background-color", 7 | "schema": "org.yorba.shotwell.preferences.ui" 8 | }, 9 | { 10 | "key": "/org/gnome/software/popular-overrides", 11 | "value": "['firefox.desktop','builder.desktop']", 12 | "signature": "as" 13 | } 14 | ], 15 | "org.libreoffice.registry": [ 16 | { 17 | "value": "true", 18 | "key": "/org/libreoffice/registry/org.openoffice.Office.Writer/Layout/Window/HorizontalRuler", 19 | "signature": "b" 20 | }, 21 | { 22 | "value": "'Our Company'", 23 | "key": "/org/libreoffice/registry/org.openoffice.UserProfile/Data/o", 24 | "signature": "s" 25 | } 26 | ], 27 | "org.gnome.online-accounts": { 28 | "Template account_fc_1490729747_0": { 29 | "FilesEnabled": false, 30 | "PhotosEnabled": false, 31 | "ContactsEnabled": false, 32 | "CalendarEnabled": true, 33 | "Provider": "google", 34 | "DocumentsEnabled": false, 35 | "PrintersEnabled": false, 36 | "MailEnabled": true 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /tests/azure/azure-pipelines.yml: -------------------------------------------------------------------------------- 1 | trigger: 2 | - master 3 | 4 | variables: 5 | - template: templates/variables-common.yml 6 | # platform specific variables, links to 7 | - template: templates/variables.yml 8 | 9 | jobs: 10 | - job: Build_and_Unittests 11 | pool: 12 | vmImage: $(VM_IMAGE) 13 | container: 14 | image: $(DOCKER_BUILD_IMAGE) 15 | steps: 16 | - template: templates/${{ variables.PREPARE_BUILD_TEMPLATE }} 17 | - template: templates/${{ variables.PREPARE_LINT_TEMPLATE }} 18 | - template: templates/${{ variables.CONFIGURE_TEMPLATE }} 19 | - script: | 20 | set -e 21 | make pylint 22 | displayName: Pylint sources 23 | - script: | 24 | set -e 25 | make blackcheck 26 | displayName: Black sources 27 | - script: | 28 | set -e 29 | make VERBOSE=1 check 30 | displayName: Run unittests 31 | - script: | 32 | set -e 33 | make VERBOSE=1 distcheck 34 | displayName: Run dist unittests 35 | - template: templates/${{ variables.BUILD_TEMPLATE }} 36 | - template: templates/publish-build.yml 37 | parameters: 38 | artifactName: 'packages-$(Build.BuildId)' 39 | targetPath: $(Build.Repository.LocalPath)/dist 40 | displayName: Publish packages 41 | -------------------------------------------------------------------------------- /COPYING.MIT: -------------------------------------------------------------------------------- 1 | Permission is hereby granted, free of charge, to any person obtaining 2 | a copy of this software and associated documentation files (the 3 | "Software"), to deal in the Software without restriction, including 4 | without limitation the rights to use, copy, modify, merge, publish, 5 | distribute, sublicense, and/or sell copies of the Software, and to 6 | permit persons to whom the Software is furnished to do so, subject to 7 | the following conditions: 8 | 9 | The above copyright notice and this permission notice shall be 10 | included in all copies or substantial portions of the Software. 11 | 12 | THE SOFTWARE IS PROVIDED "AS-IS" AND WITHOUT WARRANTY OF ANY KIND, 13 | EXPRESS, IMPLIED OR OTHERWISE, INCLUDING WITHOUT LIMITATION, ANY 14 | WARRANTY OF MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE. 15 | 16 | IN NO EVENT SHALL TOM WU BE LIABLE FOR ANY SPECIAL, INCIDENTAL, 17 | INDIRECT OR CONSEQUENTIAL DAMAGES OF ANY KIND, OR ANY DAMAGES WHATSOEVER 18 | RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER OR NOT ADVISED OF 19 | THE POSSIBILITY OF DAMAGE, AND ON ANY THEORY OF LIABILITY, ARISING OUT 20 | OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 21 | 22 | In addition, the following condition applies: 23 | 24 | All redistributions must retain an intact copy of this copyright notice 25 | and disclaimer. -------------------------------------------------------------------------------- /pylintrc: -------------------------------------------------------------------------------- 1 | [MASTER] 2 | persistent=no 3 | 4 | extension-pkg-whitelist= 5 | _ldap, 6 | samba.dcerpc.security, 7 | samba.samba3.param, 8 | samba.credentials, 9 | 10 | [MESSAGES CONTROL] 11 | enable= 12 | all, 13 | python3, 14 | 15 | disable= 16 | I, 17 | bad-continuation, 18 | bad-indentation, 19 | bad-whitespace, 20 | broad-except, 21 | dangerous-default-value, 22 | duplicate-code, 23 | fixme, 24 | invalid-name, 25 | line-too-long, 26 | missing-docstring, 27 | no-absolute-import, 28 | no-self-use, 29 | protected-access, 30 | raise-missing-from, 31 | redefined-builtin, 32 | redefined-outer-name, 33 | super-init-not-called, 34 | superfluous-parens, 35 | too-few-public-methods, 36 | too-many-arguments, 37 | too-many-branches, 38 | too-many-instance-attributes, 39 | too-many-lines, 40 | too-many-locals, 41 | too-many-nested-blocks, 42 | too-many-public-methods, 43 | too-many-return-statements, 44 | too-many-statements, 45 | trailing-newlines, 46 | trailing-whitespace, 47 | ungrouped-imports, 48 | unused-argument, 49 | wrong-import-order, 50 | wrong-import-position, 51 | consider-using-with, # pylint 2.8.0, contextmanager is not mandatory 52 | 53 | [REPORTS] 54 | output-format=colorized 55 | -------------------------------------------------------------------------------- /tests/17_fcclientad.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Copyright (c) 2015 Red Hat, Inc. 4 | # 5 | # GNOME Maps is free software; you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by the 7 | # Free Software Foundation; either version 2 of the License, or (at your 8 | # option) any later version. 9 | # 10 | # GNOME Maps is distributed in the hope that it will be useful, but 11 | # WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY 12 | # or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License 13 | # for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License along 16 | # with GNOME Maps; if not, write to the Free Software Foundation, 17 | # Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA 18 | # 19 | # Authors: Alberto Ruiz 20 | # Oliver Gutierrez 21 | 22 | if [ "x$TOPSRCDIR" = "x" ] ; then 23 | TOPSRCDIR=`pwd`/../ 24 | fi 25 | 26 | export TOPSRCDIR 27 | export PYTHONPATH=$TOPSRCDIR/_build/sub/src 28 | 29 | # We assume dbus-launch never fails 30 | eval `dbus-launch` 31 | export DBUS_SESSION_BUS_ADDRESS 32 | 33 | # Execute fleet commander dbus service tests 34 | $TOPSRCDIR/tests/_fcclientad_tests.py 35 | RET=$? 36 | 37 | kill $DBUS_SESSION_BUS_PID 38 | 39 | exit $RET 40 | -------------------------------------------------------------------------------- /COPYING.BSD3: -------------------------------------------------------------------------------- 1 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 2 | 3 | Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 4 | 5 | Neither the name of the author nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. 6 | 7 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 8 | -------------------------------------------------------------------------------- /tests/09_fcclient.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Copyright (c) 2015 Red Hat, Inc. 4 | # 5 | # GNOME Maps is free software; you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by the 7 | # Free Software Foundation; either version 2 of the License, or (at your 8 | # option) any later version. 9 | # 10 | # GNOME Maps is distributed in the hope that it will be useful, but 11 | # WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY 12 | # or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License 13 | # for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License along 16 | # with GNOME Maps; if not, write to the Free Software Foundation, 17 | # Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA 18 | # 19 | # Authors: Alberto Ruiz 20 | # Oliver Gutierrez 21 | 22 | if [ "x$TOPSRCDIR" = "x" ] ; then 23 | TOPSRCDIR=`pwd`/../ 24 | fi 25 | 26 | export TOPSRCDIR 27 | export PYTHONPATH=$TOPSRCDIR/_build/sub/src 28 | 29 | # We assume dbus-launch never fails 30 | eval `dbus-launch` 31 | export DBUS_SESSION_BUS_ADDRESS 32 | 33 | # Execute fleet commander dbus service tests 34 | $TOPSRCDIR/tests/_fcclient_tests.py 35 | RET=$? 36 | 37 | kill $DBUS_SESSION_BUS_PID 38 | 39 | #rm $TOPSRCDIR/_build/sub/client/fleetcommander/constants.pyc > /dev/null 2>&1 40 | 41 | exit $RET 42 | -------------------------------------------------------------------------------- /src/fleetcommanderclient/adapters/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # vi:ts=4 sw=4 sts=4 3 | 4 | # Copyright (C) 2017 Red Hat, Inc. 5 | # 6 | # This program is free software; you can redistribute it and/or 7 | # modify it under the terms of the GNU Lesser General Public 8 | # License as published by the Free Software Foundation; either 9 | # version 2.1 of the licence, or (at your option) any later version. 10 | # 11 | # This program is distributed in the hope that it will be useful, 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 14 | # Lesser General Public License for more details. 15 | # 16 | # You should have received a copy of the GNU Lesser General Public 17 | # License along with this program; if not, see . 18 | # 19 | # Authors: Alberto Ruiz 20 | # Oliver Gutiérrez 21 | 22 | 23 | from fleetcommanderclient.adapters.base import BaseAdapter 24 | from fleetcommanderclient.adapters.dconf import DconfAdapter 25 | from fleetcommanderclient.adapters.goa import GOAAdapter 26 | from fleetcommanderclient.adapters.nm import NetworkManagerAdapter 27 | from fleetcommanderclient.adapters.chromium import ChromiumAdapter 28 | from fleetcommanderclient.adapters.chromium import ChromeAdapter 29 | from fleetcommanderclient.adapters.firefox import FirefoxAdapter 30 | from fleetcommanderclient.adapters.firefoxbookmarks import FirefoxBookmarksAdapter 31 | -------------------------------------------------------------------------------- /src/fleetcommanderclient/configadapters/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # vi:ts=4 sw=4 sts=4 3 | 4 | # Copyright (C) 2017 Red Hat, Inc. 5 | # 6 | # This program is free software; you can redistribute it and/or 7 | # modify it under the terms of the GNU Lesser General Public 8 | # License as published by the Free Software Foundation; either 9 | # version 2.1 of the licence, or (at your option) any later version. 10 | # 11 | # This program is distributed in the hope that it will be useful, 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 14 | # Lesser General Public License for more details. 15 | # 16 | # You should have received a copy of the GNU Lesser General Public 17 | # License along with this program; if not, see . 18 | # 19 | # Authors: Alberto Ruiz 20 | # Oliver Gutiérrez 21 | 22 | 23 | from fleetcommanderclient.configadapters.dconf import DconfConfigAdapter 24 | from fleetcommanderclient.configadapters.goa import GOAConfigAdapter 25 | from fleetcommanderclient.configadapters.networkmanager import ( 26 | NetworkManagerConfigAdapter, 27 | ) 28 | from fleetcommanderclient.configadapters.chromium import ( 29 | ChromiumConfigAdapter, 30 | ChromeConfigAdapter, 31 | ) 32 | from fleetcommanderclient.configadapters.firefox import FirefoxConfigAdapter 33 | from fleetcommanderclient.configadapters.firefoxbookmarks import ( 34 | FirefoxBookmarksConfigAdapter, 35 | ) 36 | -------------------------------------------------------------------------------- /tests/data/sampleprofiledata/0050-0050-0000-0000-0000-Test1.profile: -------------------------------------------------------------------------------- 1 | { 2 | "org.freedesktop.NetworkManager": [ 3 | { 4 | "data": "{'connection': {'id': <'Company VPN'>, 'uuid': <'601d3b48-a44f-40f3-aa7a-35da4a10a099'>, 'type': <'vpn'>, 'autoconnect': , 'secondaries': <@as []>}, 'ipv6': {'method': <'auto'>, 'dns': <@aay []>, 'dns-search': <@as []>, 'address-data': <@aa{sv} []>, 'route-data': <@aa{sv} []>}, 'ipv4': {'method': <'auto'>, 'dns': <@au []>, 'dns-search': <@as []>, 'address-data': <@aa{sv} []>, 'route-data': <@aa{sv} []>}, 'vpn': {'service-type': <'org.freedesktop.NetworkManager.vpnc'>, 'data': <{'NAT Traversal Mode': 'natt', 'ipsec-secret-type': 'ask', 'IPSec secret-flags': '2', 'xauth-password-type': 'ask', 'Vendor': 'cisco', 'Xauth username': 'vpnusername', 'IPSec gateway': 'vpn.mycompany.com', 'Xauth password-flags': '2', 'IPSec ID': 'vpngroupname', 'Perfect Forward Secrecy': 'server', 'IKE DH Group': 'dh2', 'Local Port': '0'}>, 'secrets': <@a{ss} {}>}}", 5 | "type": "vpn", 6 | "uuid": "601d3b48-a44f-40f3-aa7a-35da4a10a099", 7 | "id": "Company VPN" 8 | } 9 | ], 10 | "org.gnome.gsettings": [ 11 | { 12 | "signature": "s", 13 | "value": "'#FFFFFF'", 14 | "key": "/org/yorba/shotwell/preferences/ui/background-color", 15 | "schema": "org.yorba.shotwell.preferences.ui" 16 | } 17 | ], 18 | "org.libreoffice.registry": [ 19 | { 20 | "value": "'Company'", 21 | "key": "/org/libreoffice/registry/org.openoffice.UserProfile/Data/o", 22 | "signature": "s" 23 | } 24 | ] 25 | } 26 | -------------------------------------------------------------------------------- /m4/as-ac-expand.m4: -------------------------------------------------------------------------------- 1 | dnl as-ac-expand.m4 0.2.0 -*- autoconf -*- 2 | dnl autostars m4 macro for expanding directories using configure's prefix 3 | 4 | dnl (C) 2003, 2004, 2005 Thomas Vander Stichele 5 | 6 | dnl Copying and distribution of this file, with or without modification, 7 | dnl are permitted in any medium without royalty provided the copyright 8 | dnl notice and this notice are preserved. 9 | 10 | dnl AS_AC_EXPAND(VAR, CONFIGURE_VAR) 11 | 12 | dnl example: 13 | dnl AS_AC_EXPAND(SYSCONFDIR, $sysconfdir) 14 | dnl will set SYSCONFDIR to /usr/local/etc if prefix=/usr/local 15 | 16 | AC_DEFUN([AS_AC_EXPAND], 17 | [ 18 | EXP_VAR=[$1] 19 | FROM_VAR=[$2] 20 | 21 | dnl first expand prefix and exec_prefix if necessary 22 | prefix_save=$prefix 23 | exec_prefix_save=$exec_prefix 24 | 25 | dnl if no prefix given, then use /usr/local, the default prefix 26 | if test "x$prefix" = "xNONE"; then 27 | prefix="$ac_default_prefix" 28 | fi 29 | dnl if no exec_prefix given, then use prefix 30 | if test "x$exec_prefix" = "xNONE"; then 31 | exec_prefix=$prefix 32 | fi 33 | 34 | full_var="$FROM_VAR" 35 | dnl loop until it doesn't change anymore 36 | while true; do 37 | new_full_var="`eval echo $full_var`" 38 | if test "x$new_full_var" = "x$full_var"; then break; fi 39 | full_var=$new_full_var 40 | done 41 | 42 | dnl clean up 43 | full_var=$new_full_var 44 | AC_SUBST([$1], "$full_var") 45 | 46 | dnl restore prefix and exec_prefix 47 | prefix=$prefix_save 48 | exec_prefix=$exec_prefix_save 49 | ]) 50 | 51 | -------------------------------------------------------------------------------- /src/Makefile.am: -------------------------------------------------------------------------------- 1 | fc_client_confadapt_pydir = ${fcpythondir}/fleetcommanderclient/configadapters 2 | fc_client_confadapt_py_SCRIPTS = \ 3 | fleetcommanderclient/configadapters/__init__.py \ 4 | fleetcommanderclient/configadapters/base.py \ 5 | fleetcommanderclient/configadapters/goa.py \ 6 | fleetcommanderclient/configadapters/chromium.py \ 7 | fleetcommanderclient/configadapters/firefox.py \ 8 | fleetcommanderclient/configadapters/firefoxbookmarks.py \ 9 | fleetcommanderclient/configadapters/networkmanager.py \ 10 | fleetcommanderclient/configadapters/dconf.py 11 | 12 | fc_client_adapters_pydir = ${fcpythondir}/fleetcommanderclient/adapters 13 | fc_client_adapters_py_SCRIPTS = \ 14 | fleetcommanderclient/adapters/__init__.py \ 15 | fleetcommanderclient/adapters/base.py \ 16 | fleetcommanderclient/adapters/goa.py \ 17 | fleetcommanderclient/adapters/chromium.py \ 18 | fleetcommanderclient/adapters/firefox.py \ 19 | fleetcommanderclient/adapters/firefoxbookmarks.py \ 20 | fleetcommanderclient/adapters/nm.py \ 21 | fleetcommanderclient/adapters/dconf.py 22 | 23 | 24 | fc_client_pydir = ${fcpythondir}/fleetcommanderclient 25 | fc_client_py_SCRIPTS = \ 26 | fleetcommanderclient/__init__.py \ 27 | fleetcommanderclient/configloader.py \ 28 | fleetcommanderclient/mergers.py \ 29 | fleetcommanderclient/settingscompiler.py \ 30 | fleetcommanderclient/fcadretriever.py \ 31 | fleetcommanderclient/fcclient.py \ 32 | fleetcommanderclient/fcclientad.py 33 | 34 | 35 | EXTRA_DIST = \ 36 | $(fc_client_confadapt_py_SCRIPTS) \ 37 | $(fc_client_adapters_py_SCRIPTS) \ 38 | $(fc_client_py_SCRIPTS) 39 | 40 | #CLEANFILESM = \ 41 | # $(fc_client_consts_DATA) 42 | -------------------------------------------------------------------------------- /tests/_10_mmock_realmd_dbus.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | from __future__ import absolute_import 4 | 5 | import logging 6 | 7 | import dbus.service 8 | import dbusmock 9 | import dbus.mainloop.glib 10 | from gi.repository import GLib 11 | 12 | # Set logging level to debug 13 | log = logging.getLogger() 14 | level = logging.getLevelName("DEBUG") 15 | log.setLevel(level) 16 | 17 | ml = GLib.MainLoop() 18 | 19 | dbus.mainloop.glib.DBusGMainLoop(set_as_default=True) 20 | 21 | bus = dbusmock.testcase.DBusTestCase.get_dbus() 22 | 23 | bus.add_signal_receiver( 24 | ml.quit, 25 | signal_name="Disconnected", 26 | path="/org/freedesktop/DBus/Local", 27 | dbus_interface="org.freedesktop.DBus.Local", 28 | ) 29 | 30 | realmd_bus = dbus.service.BusName( 31 | "org.freedesktop.realmd", 32 | bus, 33 | allow_replacement=True, 34 | replace_existing=True, 35 | do_not_queue=True, 36 | ) 37 | 38 | # Provider 39 | provider = dbusmock.mockobject.DBusMockObject( 40 | realmd_bus, 41 | "/org/freedesktop/realmd/Sssd", 42 | "org.freedesktop.realmd.Provider", 43 | {"Realms": ["/org/freedesktop/realmd/Sssd/fc_ipa_X"]}, 44 | ) 45 | 46 | # Realm 47 | realm = dbusmock.mockobject.DBusMockObject( 48 | realmd_bus, 49 | "/org/freedesktop/realmd/Sssd/fc_ipa_X", 50 | "org.freedesktop.realmd.Realm", 51 | { 52 | "Name": "fc.directory", 53 | "Details": [ 54 | ("server-software", "active-directory"), 55 | ("client-software", "sssd"), 56 | ], 57 | }, 58 | ) 59 | 60 | 61 | logging.debug("Configured and running realmd dbus mock") 62 | 63 | ml.run() 64 | 65 | logging.debug("Quitting realmd dbus mock") 66 | -------------------------------------------------------------------------------- /src/fleetcommanderclient/configadapters/base.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # vi:ts=4 sw=4 sts=4 3 | 4 | # Copyright (C) 2017 Red Hat, Inc. 5 | # 6 | # This program is free software; you can redistribute it and/or 7 | # modify it under the terms of the GNU Lesser General Public 8 | # License as published by the Free Software Foundation; either 9 | # version 2.1 of the licence, or (at your option) any later version. 10 | # 11 | # This program is distributed in the hope that it will be useful, 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 14 | # Lesser General Public License for more details. 15 | # 16 | # You should have received a copy of the GNU Lesser General Public 17 | # License along with this program; if not, see . 18 | # 19 | # Authors: Alberto Ruiz 20 | # Oliver Gutiérrez 21 | 22 | import os 23 | 24 | 25 | class BaseConfigAdapter: 26 | """ 27 | Base configuration adapter class 28 | """ 29 | 30 | # Namespace this config adapter handles 31 | NAMESPACE = None 32 | 33 | def bootstrap(self, uid): 34 | """ 35 | Prepare environment for a clean configuration deploy 36 | """ 37 | raise NotImplementedError("You must implement bootstrap method") 38 | 39 | def update(self, uid, data): 40 | """ 41 | Update configuration for given user 42 | """ 43 | raise NotImplementedError("You must implement update method") 44 | 45 | @staticmethod 46 | def _set_perms(fd, uid, gid, perms): 47 | """ 48 | Set owner and file mode for given file descriptor 49 | """ 50 | os.fchown(fd.fileno(), gid, uid) 51 | os.fchmod(fd.fileno(), perms) 52 | -------------------------------------------------------------------------------- /Makefile.am: -------------------------------------------------------------------------------- 1 | RPMBUILD ?= $(abs_builddir)/rpmbuild 2 | TARBALL = $(PACKAGE)-$(VERSION).tar.xz 3 | 4 | AM_DISTCHECK_CONFIGURE_FLAGS = \ 5 | --with-systemdsystemunitdir='$$dc_install_base/$(systemdsystemunitdir)' \ 6 | --with-systemduserunitdir='$$dc_install_base/$(systemduserunitdir)' 7 | 8 | SUBDIRS = data src tests 9 | 10 | EXTRA_DIST = \ 11 | $(wildcard LICENSE.*) \ 12 | $(wildcard COPYING.*) 13 | 14 | .PHONY: prep_src rpmroot rpmdistdir rpms 15 | 16 | prep_src: rpmroot dist-xz 17 | cp "$(top_builddir)/$(TARBALL)" "$(RPMBUILD)/SOURCES/" 18 | 19 | rpms: prep_src rpmroot rpmdistdir 20 | rpmbuild \ 21 | --define "_topdir $(RPMBUILD)" \ 22 | -ba \ 23 | "$(top_builddir)/$(PACKAGE).spec" 24 | cp "$(RPMBUILD)"/RPMS/*/*.rpm "$(top_builddir)/dist/rpms/" 25 | cp "$(RPMBUILD)"/SRPMS/*.src.rpm "$(top_builddir)/dist/srpms/" 26 | 27 | rpmroot: 28 | mkdir -p "$(RPMBUILD)/BUILD" 29 | mkdir -p "$(RPMBUILD)/RPMS" 30 | mkdir -p "$(RPMBUILD)/SOURCES" 31 | mkdir -p "$(RPMBUILD)/SPECS" 32 | mkdir -p "$(RPMBUILD)/SRPMS" 33 | 34 | rpmdistdir: 35 | mkdir -p "$(top_builddir)/dist/rpms" 36 | mkdir -p "$(top_builddir)/dist/srpms" 37 | 38 | clean-local: 39 | rm -rf "$(RPMBUILD)" 40 | rm -rf "$(top_builddir)/dist" 41 | rm -f "$(top_builddir)"/$(PACKAGE)-*.tar.gz 42 | 43 | .PHONY: pylint 44 | pylint: 45 | FILES=`find $(top_srcdir) \ 46 | -type d -exec test -e '{}/__init__.py' \; -print -prune -o \ 47 | -path './rpmbuild' -prune -o \ 48 | -name '*.py' -print`; \ 49 | echo -e "Pylinting files:\n$${FILES}\n"; \ 50 | $(PYTHON) -m pylint --version; \ 51 | PYTHONPATH=$(top_srcdir):$(top_srcdir)/admin $(PYTHON) -m pylint \ 52 | --rcfile=$(top_srcdir)/pylintrc \ 53 | --load-plugins pylint_plugins \ 54 | $${FILES} 55 | 56 | .PHONY: black 57 | black: 58 | $(PYTHON) -m black -v \ 59 | $(top_srcdir) 60 | 61 | .PHONY: blackcheck 62 | blackcheck: 63 | $(PYTHON) -m black -v --check --diff \ 64 | $(top_srcdir) 65 | 66 | -------------------------------------------------------------------------------- /data/Makefile.am: -------------------------------------------------------------------------------- 1 | fc_client_dbus_servicedir = ${datarootdir}/dbus-1/system-services/ 2 | fc_client_dbus_service_in_files = org.freedesktop.FleetCommanderClient.service.in 3 | fc_client_dbus_service_DATA = org.freedesktop.FleetCommanderClient.service 4 | 5 | fc_client_dbus_configdir = ${sysconfdir}/dbus-1/system.d/ 6 | fc_client_dbus_config_DATA = org.freedesktop.FleetCommanderClient.conf 7 | 8 | fc_client_systemd_servicedir = $(systemdsystemunitdir) 9 | fc_client_systemd_service_in_files = fleet-commander-client.service.in 10 | fc_client_systemd_service_DATA = fleet-commander-client.service 11 | 12 | 13 | fc_client_ad_dbus_servicedir = ${datarootdir}/dbus-1/system-services/ 14 | fc_client_ad_dbus_service_in_files = org.freedesktop.FleetCommanderClientAD.service.in 15 | fc_client_ad_dbus_service_DATA = org.freedesktop.FleetCommanderClientAD.service 16 | 17 | fc_client_ad_dbus_configdir = ${sysconfdir}/dbus-1/system.d/ 18 | fc_client_ad_dbus_config_DATA = org.freedesktop.FleetCommanderClientAD.conf 19 | 20 | fc_client_ad_systemd_servicedir = $(systemdsystemunitdir) 21 | fc_client_ad_systemd_service_in_files = fleet-commander-clientad.service.in 22 | fc_client_ad_systemd_service_DATA = fleet-commander-clientad.service 23 | 24 | 25 | fc_client_adretriever_systemd_servicedir = $(systemduserunitdir) 26 | fc_client_adretriever_systemd_service_in_files = fleet-commander-adretriever.service.in 27 | fc_client_adretriever_systemd_service_DATA = fleet-commander-adretriever.service 28 | 29 | 30 | fc_client_configdir = ${sysconfdir}/xdg/ 31 | fc_client_config_DATA = fleet-commander-client.conf 32 | 33 | EXTRA_DIST = \ 34 | $(fc_client_dbus_service_DATA) \ 35 | $(fc_client_dbus_config_DATA) \ 36 | $(fc_client_systemd_service_DATA) \ 37 | $(fc_client_ad_dbus_service_DATA) \ 38 | $(fc_client_ad_dbus_config_DATA) \ 39 | $(fc_client_ad_systemd_service_DATA) \ 40 | $(fc_client_adretriever_systemd_service_DATA) \ 41 | $(fc_client_config_DATA) 42 | 43 | CLEANFILES = \ 44 | $(fc_client_dbus_service_DATA) \ 45 | $(fc_client_systemd_service_DATA) \ 46 | $(fc_client_ad_dbus_service_DATA) \ 47 | $(fc_client_ad_systemd_service_DATA) \ 48 | $(fc_client_adretriever_systemd_service_DATA) -------------------------------------------------------------------------------- /src/fleetcommanderclient/configloader.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # vi:ts=4 sw=4 sts=4 3 | 4 | # Copyright (C) 2017 Red Hat, Inc. 5 | # 6 | # This program is free software; you can redistribute it and/or 7 | # modify it under the terms of the GNU Lesser General Public 8 | # License as published by the Free Software Foundation; either 9 | # version 2.1 of the licence, or (at your option) any later version. 10 | # 11 | # This program is distributed in the hope that it will be useful, 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 14 | # Lesser General Public License for more details. 15 | # 16 | # You should have received a copy of the GNU Lesser General Public 17 | # License along with this program; if not, see . 18 | # 19 | # Authors: Alberto Ruiz 20 | # Oliver Gutiérrez 21 | 22 | import logging 23 | 24 | from gi.repository import GLib 25 | 26 | 27 | class ConfigLoader: 28 | 29 | DEFAULTS = { 30 | "dconf_db_path": "/etc/dconf/db", 31 | "dconf_profile_path": "/run/dconf/user", 32 | "goa_run_path": "/run/goa-1.0", 33 | "chromium_policies_path": "/etc/chromium/policies/managed", 34 | "chrome_policies_path": "/etc/opt/chrome/policies/managed", 35 | "firefox_prefs_path": "/etc/firefox/pref", 36 | "firefox_policies_path": "/run/user/{}/firefox", 37 | "log_level": "info", 38 | } 39 | 40 | def __init__(self, configfile): 41 | try: 42 | self.configfile = configfile 43 | self.keyfile = GLib.KeyFile.new() 44 | self.keyfile.load_from_file(configfile, GLib.KeyFileFlags.NONE) 45 | except Exception as e: 46 | logging.warning( 47 | "Can not load config file %s. Using defaults. %s", configfile, e 48 | ) 49 | 50 | def get_value(self, key): 51 | try: 52 | return self.keyfile.get_string("fleet-commander", key) 53 | except Exception as e: 54 | if key in self.DEFAULTS.keys(): 55 | return self.DEFAULTS[key] 56 | logging.warning("Can not read key %s from config: %s", key, e) 57 | return None 58 | -------------------------------------------------------------------------------- /tests/data/test_profile.json: -------------------------------------------------------------------------------- 1 | { 2 | "org.freedesktop.NetworkManager": [ 3 | { 4 | "data": "{'connection': {'id': <'Company VPN'>, 'uuid': <'601d3b48-a44f-40f3-aa7a-35da4a10a099'>, 'type': <'vpn'>, 'autoconnect': , 'secondaries': <@as []>}, 'ipv6': {'method': <'auto'>, 'dns': <@aay []>, 'dns-search': <@as []>, 'address-data': <@aa{sv} []>, 'route-data': <@aa{sv} []>}, 'ipv4': {'method': <'auto'>, 'dns': <@au []>, 'dns-search': <@as []>, 'address-data': <@aa{sv} []>, 'route-data': <@aa{sv} []>}, 'vpn': {'service-type': <'org.freedesktop.NetworkManager.vpnc'>, 'data': <{'NAT Traversal Mode': 'natt', 'ipsec-secret-type': 'ask', 'IPSec secret-flags': '2', 'xauth-password-type': 'ask', 'Vendor': 'cisco', 'Xauth username': 'vpnusername', 'IPSec gateway': 'vpn.mycompany.com', 'Xauth password-flags': '2', 'IPSec ID': 'vpngroupname', 'Perfect Forward Secrecy': 'server', 'IKE DH Group': 'dh2', 'Local Port': '0'}>, 'secrets': <@a{ss} {}>}}", 5 | "type": "vpn", 6 | "uuid": "601d3b48-a44f-40f3-aa7a-35da4a10a099", 7 | "id": "Company VPN" 8 | } 9 | ], 10 | 11 | "org.gnome.gsettings": [ 12 | { 13 | "signature": "s", 14 | "value": "'#FFFFFF'", 15 | "key": "/org/yorba/shotwell/preferences/ui/background-color", 16 | "schema": "org.yorba.shotwell.preferences.ui" 17 | } 18 | ], 19 | 20 | "org.libreoffice.registry": [ 21 | { 22 | "value": "'Company'", 23 | "key": "/org/libreoffice/registry/org.openoffice.UserProfile/Data/o", 24 | "signature": "s" 25 | } 26 | ], 27 | 28 | "org.gnome.online-accounts": { 29 | "Template account_fc_1490729747_0": { 30 | "FilesEnabled": true, 31 | "PhotosEnabled": false, 32 | "ContactsEnabled": false, 33 | "CalendarEnabled": true, 34 | "Provider": "google", 35 | "DocumentsEnabled": false, 36 | "PrintersEnabled": true, 37 | "MailEnabled": true 38 | }, 39 | "Template account_fc_1490729585_0": { 40 | "PhotosEnabled": false, 41 | "Provider": "facebook", 42 | "MapsEnabled": false 43 | } 44 | }, 45 | 46 | "org.mozilla.firefox.Bookmarks": [ 47 | { 48 | "key": "blah", 49 | "value": { 50 | "Title": "Test bookmark", 51 | "URL": "https://example.com", 52 | "Favicon": "https://example.com/favicon.ico", 53 | "Placement": "toolbar", 54 | "Folder": "FolderName" 55 | } 56 | } 57 | ] 58 | 59 | } -------------------------------------------------------------------------------- /tests/test_fcclient_service.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | # vi:ts=4 sw=4 sts=4 4 | 5 | # Copyright (C) 2015 Red Hat, Inc. 6 | # 7 | # This program is free software; you can redistribute it and/or 8 | # modify it under the terms of the GNU Lesser General Public 9 | # License as published by the Free Software Foundation; either 10 | # version 2.1 of the licence, or (at your option) any later version. 11 | # 12 | # This program is distributed in the hope that it will be useful, 13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 15 | # Lesser General Public License for more details. 16 | # 17 | # You should have received a copy of the GNU Lesser General Public 18 | # License along with this program; if not, see . 19 | # 20 | # Authors: Alberto Ruiz 21 | # Oliver Gutiérrez 22 | 23 | # Python imports 24 | import os 25 | import sys 26 | 27 | import dbus 28 | 29 | PYTHONPATH = os.path.join(os.environ["TOPSRCDIR"], "src") 30 | sys.path.append(PYTHONPATH) 31 | 32 | # Fleet commander imports 33 | from fleetcommanderclient import fcclient 34 | from fleetcommanderclient.configloader import ConfigLoader 35 | 36 | 37 | class TestConfigLoader(ConfigLoader): 38 | pass 39 | 40 | 41 | class FakeNMConfigAdapter: 42 | """ 43 | Fake configuration adapter for Network Manager 44 | """ 45 | 46 | NAMESPACE = "org.freedesktop.NetworkManager" 47 | 48 | def bootstrap(self, uid): 49 | 50 | pass 51 | 52 | def update(self, uid, data): 53 | pass 54 | 55 | 56 | fcclient.configadapters.NetworkManagerConfigAdapter = FakeNMConfigAdapter 57 | 58 | 59 | class TestFleetCommanderClientDbusService(fcclient.FleetCommanderClientDbusService): 60 | def __init__(self): 61 | 62 | # Create a config loader that loads modified defaults 63 | self.tmpdir = sys.argv[1] 64 | 65 | TestConfigLoader.DEFAULTS = { 66 | "dconf_db_path": os.path.join(self.tmpdir, "etc/dconf/db"), 67 | "dconf_profile_path": os.path.join(self.tmpdir, "run/dconf/user"), 68 | "goa_run_path": os.path.join(self.tmpdir, "run/goa-1.0"), 69 | "log_level": "info", 70 | } 71 | 72 | fcclient.ConfigLoader = TestConfigLoader 73 | 74 | super().__init__(configfile="NON_EXISTENT") 75 | 76 | @dbus.service.method( 77 | fcclient.DBUS_INTERFACE_NAME, in_signature="", out_signature="b" 78 | ) 79 | def TestServiceAlive(self): 80 | return True 81 | 82 | 83 | if __name__ == "__main__": 84 | TestFleetCommanderClientDbusService().run(sessionbus=True) 85 | -------------------------------------------------------------------------------- /tests/00_configloader.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | # vi:ts=4 sw=4 sts=4 4 | 5 | # Copyright (C) 2017 Red Hat, Inc. 6 | # 7 | # This program is free software; you can redistribute it and/or 8 | # modify it under the terms of the GNU Lesser General Public 9 | # License as published by the Free Software Foundation; either 10 | # version 2.1 of the licence, or (at your option) any later version. 11 | # 12 | # This program is distributed in the hope that it will be useful, 13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 15 | # Lesser General Public License for more details. 16 | # 17 | # You should have received a copy of the GNU Lesser General Public 18 | # License along with this program; if not, see . 19 | # 20 | # Authors: Alberto Ruiz 21 | # Oliver Gutiérrez 22 | 23 | # Python imports 24 | import os 25 | import sys 26 | import unittest 27 | 28 | PYTHONPATH = os.path.join(os.environ["TOPSRCDIR"], "src") 29 | sys.path.append(PYTHONPATH) 30 | 31 | # Fleet commander imports 32 | from fleetcommanderclient.configloader import ConfigLoader 33 | 34 | 35 | class TestConfigLoader(unittest.TestCase): 36 | 37 | maxDiff = None 38 | 39 | def test_00_load_inexistent_config_file(self): 40 | # Load inexistent configuration file 41 | configfile = os.path.join( 42 | os.environ["TOPSRCDIR"], "tests/data/inexistent_config_file.conf" 43 | ) 44 | config = ConfigLoader(configfile) 45 | # Read a non existent key without default specified 46 | result = config.get_value("inexistent_key") 47 | self.assertEqual(result, None) 48 | # Read non existent key but with default value 49 | for key, value in config.DEFAULTS.items(): 50 | result = config.get_value(key) 51 | self.assertEqual(result, value) 52 | 53 | def test_01_load_config_file(self): 54 | # Load inexistent configuration file 55 | configfile = os.path.join( 56 | os.environ["TOPSRCDIR"], "tests/data/test_config_file.conf" 57 | ) 58 | config = ConfigLoader(configfile) 59 | # Read a non existent key without default specified 60 | result = config.get_value("inexistent_key") 61 | self.assertEqual(result, None) 62 | # Read non existent key but with default value 63 | result = config.get_value("log_level") 64 | self.assertEqual(result, config.DEFAULTS["log_level"]) 65 | # Read existent key 66 | result = config.get_value("goa_run_path") 67 | self.assertEqual(result, "/run/goa-1.0") 68 | 69 | 70 | if __name__ == "__main__": 71 | unittest.main() 72 | -------------------------------------------------------------------------------- /src/fleetcommanderclient/configadapters/chromium.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # vi:ts=4 sw=4 sts=4 3 | 4 | # Copyright (C) 2017 Red Hat, Inc. 5 | # 6 | # This program is free software; you can redistribute it and/or 7 | # modify it under the terms of the GNU Lesser General Public 8 | # License as published by the Free Software Foundation; either 9 | # version 2.1 of the licence, or (at your option) any later version. 10 | # 11 | # This program is distributed in the hope that it will be useful, 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 14 | # Lesser General Public License for more details. 15 | # 16 | # You should have received a copy of the GNU Lesser General Public 17 | # License along with this program; if not, see . 18 | # 19 | # Authors: Alberto Ruiz 20 | # Oliver Gutiérrez 21 | 22 | import os 23 | import logging 24 | import json 25 | 26 | from fleetcommanderclient.configadapters.base import BaseConfigAdapter 27 | 28 | 29 | class ChromiumConfigAdapter(BaseConfigAdapter): 30 | """ 31 | Chromium config adapter 32 | """ 33 | 34 | NAMESPACE = "org.chromium.Policies" 35 | POLICIES_FILENAME = "fleet-commander-%s.json" 36 | 37 | def __init__(self, policies_path): 38 | self.policies_path = policies_path 39 | 40 | def bootstrap(self, uid): 41 | filename = self.POLICIES_FILENAME % uid 42 | path = os.path.join(self.policies_path, filename) 43 | # Delete file at managed profiles 44 | logging.debug('Removing previous policies file: "%s"', path) 45 | try: 46 | os.remove(path) 47 | except Exception: 48 | pass 49 | 50 | def update(self, uid, data): 51 | filename = self.POLICIES_FILENAME % uid 52 | path = os.path.join(self.policies_path, filename) 53 | # Create policies path 54 | try: 55 | os.makedirs(self.policies_path) 56 | except Exception: 57 | pass 58 | # Prepare data 59 | policies = {} 60 | for item in data: 61 | if "key" in item and "value" in item: 62 | policies[item["key"]] = item["value"] 63 | # Write policies data 64 | logging.debug('Writing %s data to: "%s"', self.NAMESPACE, path) 65 | with open(path, "w") as fd: 66 | # Change permissions and ownership permisions 67 | self._set_perms(fd, uid, -1, 0o640) 68 | fd.write(json.dumps(policies)) 69 | fd.close() 70 | 71 | 72 | class ChromeConfigAdapter(ChromiumConfigAdapter): 73 | """ 74 | Chrome config adapter 75 | """ 76 | 77 | NAMESPACE = "org.google.chrome.Policies" 78 | -------------------------------------------------------------------------------- /src/fleetcommanderclient/configadapters/firefox.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # vi:ts=4 sw=4 sts=4 3 | 4 | # Copyright (C) 2018 Red Hat, Inc. 5 | # 6 | # This program is free software; you can redistribute it and/or 7 | # modify it under the terms of the GNU Lesser General Public 8 | # License as published by the Free Software Foundation; either 9 | # version 2.1 of the licence, or (at your option) any later version. 10 | # 11 | # This program is distributed in the hope that it will be useful, 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 14 | # Lesser General Public License for more details. 15 | # 16 | # You should have received a copy of the GNU Lesser General Public 17 | # License along with this program; if not, see . 18 | # 19 | # Authors: Alberto Ruiz 20 | # Oliver Gutiérrez 21 | 22 | import os 23 | import logging 24 | import json 25 | 26 | from fleetcommanderclient.configadapters.base import BaseConfigAdapter 27 | 28 | 29 | class FirefoxConfigAdapter(BaseConfigAdapter): 30 | """ 31 | Firefox config adapter 32 | """ 33 | 34 | NAMESPACE = "org.mozilla.firefox" 35 | PREFS_FILENAME = "fleet-commander-%s.json" 36 | PREF_TEMPLATE = 'pref("%s", %s);' 37 | 38 | def __init__(self, preferences_path): 39 | self.preferences_path = preferences_path 40 | 41 | def bootstrap(self, uid): 42 | filename = self.PREFS_FILENAME % uid 43 | path = os.path.join(self.preferences_path, filename) 44 | # Delete file at preferences path 45 | logging.debug('Removing previous preferences file: "%s"', path) 46 | try: 47 | os.remove(path) 48 | except Exception: 49 | pass 50 | 51 | def update(self, uid, data): 52 | logging.debug("Updating %s. Data received: %s", self.NAMESPACE, data) 53 | filename = self.PREFS_FILENAME % uid 54 | path = os.path.join(self.preferences_path, filename) 55 | # Create preferences path 56 | try: 57 | os.makedirs(self.preferences_path) 58 | except Exception: 59 | pass 60 | # Prepare data 61 | preferences = [] 62 | for item in data: 63 | if "key" in item and "value" in item: 64 | # TODO: Check for locked settings and use lockPref inst 65 | preferences.append( 66 | self.PREF_TEMPLATE % (item["key"], json.dumps(item["value"])) 67 | ) 68 | # Write preferences data 69 | logging.debug('Writing %s data to: "%s"', self.NAMESPACE, path) 70 | with open(path, "w") as fd: 71 | # Change permissions and ownership permisions 72 | self._set_perms(fd, uid, -1, 0o640) 73 | fd.write("\n".join(preferences)) 74 | fd.close() 75 | -------------------------------------------------------------------------------- /src/fleetcommanderclient/configadapters/firefoxbookmarks.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # vi:ts=4 sw=4 sts=4 3 | 4 | # Copyright (C) 2019 Red Hat, Inc. 5 | # 6 | # This program is free software; you can redistribute it and/or 7 | # modify it under the terms of the GNU Lesser General Public 8 | # License as published by the Free Software Foundation; either 9 | # version 2.1 of the licence, or (at your option) any later version. 10 | # 11 | # This program is distributed in the hope that it will be useful, 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 14 | # Lesser General Public License for more details. 15 | # 16 | # You should have received a copy of the GNU Lesser General Public 17 | # License along with this program; if not, see . 18 | # 19 | # Author: Oliver Gutiérrez 20 | 21 | import os 22 | import logging 23 | import json 24 | 25 | from fleetcommanderclient.configadapters.base import BaseConfigAdapter 26 | 27 | 28 | class FirefoxBookmarksConfigAdapter(BaseConfigAdapter): 29 | """ 30 | Firefox Bookmarks config adapter 31 | """ 32 | 33 | NAMESPACE = "org.mozilla.firefox.Bookmarks" 34 | POLICIES_FILENAME = "policies.json" 35 | 36 | def __init__(self, policies_path): 37 | logging.debug( 38 | "Initialized firefox bookmarks config adapter with policies path %s", 39 | policies_path, 40 | ) 41 | self.policies_path = policies_path 42 | 43 | def bootstrap(self, uid): 44 | path = os.path.join(self.policies_path.format(uid), self.POLICIES_FILENAME) 45 | # Delete existing files 46 | logging.debug('Removing previous policies file: "%s"', path) 47 | try: 48 | os.remove(path) 49 | except Exception as e: 50 | logging.debug('Error removing previous policies file "%s": %s', path, e) 51 | 52 | def update(self, uid, data): 53 | logging.debug("Updating %s. Data received: %s", self.NAMESPACE, data) 54 | directory = self.policies_path.format(uid) 55 | path = os.path.join(directory, self.POLICIES_FILENAME) 56 | # Create directory 57 | try: 58 | os.makedirs(directory) 59 | except Exception: 60 | pass 61 | # Prepare data 62 | bookmarks = [] 63 | for item in data: 64 | if "key" in item and "value" in item: 65 | bookmarks.append(item["value"]) 66 | policies_data = {"policies": {"Bookmarks": bookmarks}} 67 | # Write preferences data 68 | logging.debug('Writing %s data to: "%s"', self.NAMESPACE, path) 69 | with open(path, "w") as fd: 70 | # Change permissions and ownership permisions 71 | self._set_perms(fd, uid, -1, 0o640) 72 | fd.write(json.dumps(policies_data)) 73 | fd.close() 74 | -------------------------------------------------------------------------------- /tests/data/sampleprofiledata/0090-0090-0000-0000-0000-Test3.profile: -------------------------------------------------------------------------------- 1 | { 2 | "org.freedesktop.NetworkManager": [ 3 | { 4 | "data": "{'connection': {'id': <'Company VPN'>, 'uuid': <'601d3b48-a44f-40f3-aa7a-35da4a10a099'>, 'type': <'vpn'>, 'autoconnect': , 'secondaries': <@as []>}, 'ipv6': {'method': <'auto'>, 'dns': <@aay []>, 'dns-search': <@as []>, 'address-data': <@aa{sv} []>, 'route-data': <@aa{sv} []>}, 'ipv4': {'method': <'auto'>, 'dns': <@au []>, 'dns-search': <@as []>, 'address-data': <@aa{sv} []>, 'route-data': <@aa{sv} []>}, 'vpn': {'service-type': <'org.freedesktop.NetworkManager.vpnc'>, 'data': <{'NAT Traversal Mode': 'natt', 'ipsec-secret-type': 'ask', 'IPSec secret-flags': '2', 'xauth-password-type': 'ask', 'Vendor': 'cisco', 'Xauth username': 'vpnusername', 'IPSec gateway': 'vpn.mycompany.com', 'Xauth password-flags': '2', 'IPSec ID': 'vpngroupname', 'Perfect Forward Secrecy': 'server', 'IKE DH Group': 'dh2', 'Local Port': '0'}>, 'secrets': <@a{ss} {}>}}", 5 | "type": "vpn", 6 | "uuid": "601d3b48-a44f-40f3-aa7a-35da4a10a099", 7 | "id": "The Company VPN" 8 | }, 9 | { 10 | "data": "{'connection': {'id': <'Intranet VPN'>, 'uuid': <'0be7d422-1635-11e7-a83f-68f728db19d3'>, 'type': <'vpn'>, 'autoconnect': , 'secondaries': <@as []>}, 'ipv6': {'method': <'auto'>, 'dns': <@aay []>, 'dns-search': <@as []>, 'address-data': <@aa{sv} []>, 'route-data': <@aa{sv} []>}, 'ipv4': {'method': <'auto'>, 'dns': <@au []>, 'dns-search': <@as []>, 'address-data': <@aa{sv} []>, 'route-data': <@aa{sv} []>}, 'vpn': {'service-type': <'org.freedesktop.NetworkManager.vpnc'>, 'data': <{'NAT Traversal Mode': 'natt', 'ipsec-secret-type': 'ask', 'IPSec secret-flags': '2', 'xauth-password-type': 'ask', 'Vendor': 'cisco', 'Xauth username': 'vpnusername', 'IPSec gateway': 'vpn.mycompany.com', 'Xauth password-flags': '2', 'IPSec ID': 'vpngroupname', 'Perfect Forward Secrecy': 'server', 'IKE DH Group': 'dh2', 'Local Port': '0'}>, 'secrets': <@a{ss} {}>}}", 11 | "type": "vpn", 12 | "uuid": "0be7d422-1635-11e7-a83f-68f728db19d3", 13 | "id": "Intranet VPN" 14 | } 15 | ], 16 | "org.gnome.gsettings": [ 17 | { 18 | "key": "/org/gnome/software/popular-overrides", 19 | "value": "['riot.desktop','matrix.desktop']", 20 | "signature": "as" 21 | } 22 | ], 23 | "org.libreoffice.registry": [ 24 | { 25 | "value": "'The Company'", 26 | "key": "/org/libreoffice/registry/org.openoffice.UserProfile/Data/o", 27 | "signature": "s" 28 | } 29 | ], 30 | "org.gnome.online-accounts": { 31 | "Template account_fc_1490729747_0": { 32 | "FilesEnabled": true, 33 | "PhotosEnabled": false, 34 | "ContactsEnabled": false, 35 | "CalendarEnabled": true, 36 | "Provider": "google", 37 | "DocumentsEnabled": false, 38 | "PrintersEnabled": true, 39 | "MailEnabled": true 40 | }, 41 | "Template account_fc_1490729585_0": { 42 | "PhotosEnabled": false, 43 | "Provider": "facebook", 44 | "MapsEnabled": false 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/fleetcommanderclient/configadapters/goa.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # vi:ts=4 sw=4 sts=4 3 | 4 | # Copyright (C) 2017 Red Hat, Inc. 5 | # 6 | # This program is free software; you can redistribute it and/or 7 | # modify it under the terms of the GNU Lesser General Public 8 | # License as published by the Free Software Foundation; either 9 | # version 2.1 of the licence, or (at your option) any later version. 10 | # 11 | # This program is distributed in the hope that it will be useful, 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 14 | # Lesser General Public License for more details. 15 | # 16 | # You should have received a copy of the GNU Lesser General Public 17 | # License along with this program; if not, see . 18 | # 19 | # Authors: Alberto Ruiz 20 | # Oliver Gutiérrez 21 | 22 | import os 23 | import shutil 24 | import logging 25 | 26 | from gi.repository import GLib 27 | 28 | from fleetcommanderclient.configadapters.base import BaseConfigAdapter 29 | 30 | 31 | class GOAConfigAdapter(BaseConfigAdapter): 32 | """ 33 | Configuration adapter for GNOME Online Accounts 34 | """ 35 | 36 | NAMESPACE = "org.gnome.online-accounts" 37 | FC_ACCOUNTS_FILE = "fleet-commander-accounts.conf" 38 | 39 | def __init__(self, goa_runtime_path): 40 | self.goa_runtime_path = goa_runtime_path 41 | 42 | def bootstrap(self, uid): 43 | runtime_path = os.path.join(self.goa_runtime_path, str(uid)) 44 | logging.debug('Removing runtime path for GOA: "%s"', runtime_path) 45 | try: 46 | shutil.rmtree(runtime_path) 47 | except Exception as e: 48 | logging.warning('Error removing GOA runtime path "%s": %s', runtime_path, e) 49 | 50 | def update(self, uid, data): 51 | # Create runtime path 52 | runtime_path = os.path.join(self.goa_runtime_path, str(uid)) 53 | logging.debug('Creating runtime path for GOA: "%s"', runtime_path) 54 | try: 55 | os.makedirs(runtime_path) 56 | except Exception as e: 57 | logging.error('Error creating GOA runtime path "%s": %s', runtime_path, e) 58 | return 59 | 60 | # Prepare data for saving it in keyfile 61 | logging.debug("Preparing GOA data for saving to keyfile") 62 | keyfile = GLib.KeyFile.new() 63 | for account, accountdata in data.items(): 64 | for key, value in accountdata.items(): 65 | if isinstance(value, bool): 66 | keyfile.set_boolean(account, key, value) 67 | else: 68 | keyfile.set_string(account, key, value) 69 | 70 | # Save config file 71 | keyfile_path = os.path.join(runtime_path, self.FC_ACCOUNTS_FILE) 72 | logging.debug('Saving GOA keyfile to "%s"', keyfile_path) 73 | try: 74 | keyfile.save_to_file(keyfile_path) 75 | except Exception as e: 76 | logging.error('Error saving GOA keyfile at "%s": %s', keyfile_path, e) 77 | return 78 | 79 | logging.info("Processed GOA configuration for UID %s", uid) 80 | -------------------------------------------------------------------------------- /tests/test_fcclientad_service.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | # vi:ts=4 sw=4 sts=4 4 | 5 | # Copyright (C) 2015 Red Hat, Inc. 6 | # 7 | # This program is free software; you can redistribute it and/or 8 | # modify it under the terms of the GNU Lesser General Public 9 | # License as published by the Free Software Foundation; either 10 | # version 2.1 of the licence, or (at your option) any later version. 11 | # 12 | # This program is distributed in the hope that it will be useful, 13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 15 | # Lesser General Public License for more details. 16 | # 17 | # You should have received a copy of the GNU Lesser General Public 18 | # License along with this program; if not, see . 19 | # 20 | # Authors: Alberto Ruiz 21 | # Oliver Gutiérrez 22 | 23 | # Python imports 24 | import os 25 | import sys 26 | 27 | import dbus 28 | 29 | PYTHONPATH = os.path.join(os.environ["TOPSRCDIR"], "src") 30 | sys.path.append(PYTHONPATH) 31 | 32 | # Fleet commander imports 33 | from fleetcommanderclient import fcclientad 34 | from fleetcommanderclient.configloader import ConfigLoader 35 | from fleetcommanderclient.adapters import goa 36 | 37 | USER_NAME = "myuser" 38 | USER_UID = 55555 39 | 40 | 41 | def mocked_uname(uid): 42 | """ 43 | This is a mock for os.pwd.getpwuid 44 | """ 45 | 46 | class MockPwd: 47 | pw_name = USER_NAME 48 | pw_dir = sys.argv[1] 49 | 50 | if uid == USER_UID: 51 | return MockPwd() 52 | raise Exception("Unknown UID: %d" % uid) 53 | 54 | 55 | def universal_function(*args, **kwargs): 56 | pass 57 | 58 | 59 | # Monkey patch chown function in os module for chromium config adapter 60 | goa.os.chown = universal_function 61 | 62 | 63 | class TestConfigLoader(ConfigLoader): 64 | pass 65 | 66 | 67 | class TestFleetCommanderClientADDbusService( 68 | fcclientad.FleetCommanderClientADDbusService 69 | ): 70 | 71 | TEST_UUID = 55555 72 | 73 | def __init__(self): 74 | 75 | # Create a config loader that loads modified defaults 76 | self.tmpdir = sys.argv[1] 77 | 78 | TestConfigLoader.DEFAULTS = { 79 | "dconf_db_path": os.path.join(self.tmpdir, "etc/dconf/db"), 80 | "dconf_profile_path": os.path.join(self.tmpdir, "run/dconf/user"), 81 | "goa_run_path": os.path.join(self.tmpdir, "run/goa-1.0"), 82 | "log_level": "info", 83 | } 84 | 85 | fcclientad.ConfigLoader = TestConfigLoader 86 | 87 | super().__init__(configfile="NON_EXISTENT") 88 | 89 | # Put all adapters in test mode 90 | for adapter in self.adapters.values(): 91 | adapter._TEST_CACHE_PATH = os.path.join(self.tmpdir, "cache") 92 | 93 | def get_peer_uid(self, sender): 94 | return self.TEST_UUID 95 | 96 | @dbus.service.method( 97 | fcclientad.DBUS_INTERFACE_NAME, in_signature="", out_signature="b" 98 | ) 99 | def TestServiceAlive(self): 100 | return True 101 | 102 | 103 | if __name__ == "__main__": 104 | TestFleetCommanderClientADDbusService().run(sessionbus=True) 105 | -------------------------------------------------------------------------------- /tests/smbmock.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # vi:ts=2 sw=2 sts=2 3 | 4 | # Copyright (C) 2019 Red Hat, Inc. 5 | # 6 | # This program is free software; you can redistribute it and/or 7 | # modify it under the terms of the GNU Lesser General Public 8 | # License as published by the Free Software Foundation; either 9 | # version 2.1 of the licence, or (at your option) any later version. 10 | # 11 | # This program is distributed in the hope that it will be useful, 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 14 | # Lesser General Public License for more details. 15 | # 16 | # You should have received a copy of the GNU Lesser General Public 17 | # License along with this program; if not, see . 18 | # 19 | # Authors: Alberto Ruiz 20 | # Oliver Gutiérrez 21 | 22 | import os 23 | import shutil 24 | import json 25 | import logging 26 | 27 | # Temporary directory. Set on each test run at setUp() 28 | TEMP_DIR = None 29 | 30 | 31 | class SMBMock: 32 | def __init__(self, servername, service, lp, creds, sign=False): 33 | logging.debug("SMBMock: Mocking SMB at \\\\%s\\%s", servername, service) 34 | self.tempdir = TEMP_DIR 35 | logging.debug("Using temporary directory at %s", self.tempdir) 36 | self.profilesdir = os.path.join(self.tempdir, "%s/Policies" % servername) 37 | if not os.path.exists(self.profilesdir): 38 | os.makedirs(self.profilesdir) 39 | 40 | def _translate_path(self, uri): 41 | return os.path.join(self.tempdir, uri.replace("\\", "/")) 42 | 43 | def loadfile(self, furi): 44 | logging.debug("SMBMock: LOADFILE %s", furi) 45 | path = self._translate_path(furi) 46 | with open(path, "rb") as fd: 47 | data = fd.read() 48 | fd.close() 49 | return data 50 | 51 | def savefile(self, furi, data): 52 | logging.debug("SMBMock: SAVEFILE %s", furi) 53 | path = self._translate_path(furi) 54 | with open(path, "wb") as fd: 55 | fd.write(data) 56 | fd.close() 57 | logging.debug("SMBMock: Written %s", path) 58 | 59 | def chkpath(self, duri): 60 | logging.debug("SMBMock: CHKPATH %s", duri) 61 | path = self._translate_path(duri) 62 | return os.path.exists(path) 63 | 64 | def mkdir(self, duri): 65 | logging.debug("SMBMock: MKDIR %s", duri) 66 | path = self._translate_path(duri) 67 | if not os.path.exists(path): 68 | os.makedirs(path) 69 | 70 | def set_acl(self, duri, fssd, sio): 71 | logging.debug("SMBMock: SETACL %s", duri) 72 | path = self._translate_path(duri) 73 | aclpath = os.path.join(path, "__acldata__.json") 74 | acldata = json.dumps( 75 | { 76 | "uri": duri, 77 | "sio": sio, 78 | "fssd": fssd.as_sddl(), 79 | } 80 | ) 81 | with open(aclpath, "w") as fd: 82 | fd.write(acldata) 83 | fd.close() 84 | 85 | def deltree(self, duri): 86 | logging.debug("SMBMock: DELTREE %s", duri) 87 | path = self._translate_path(duri) 88 | if os.path.exists(path): 89 | shutil.rmtree(path) 90 | 91 | 92 | SMB = SMBMock 93 | -------------------------------------------------------------------------------- /src/fleetcommanderclient/adapters/firefoxbookmarks.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # vi:ts=4 sw=4 sts=4 3 | 4 | # Copyright (C) 2017 Red Hat, Inc. 5 | # 6 | # This program is free software; you can redistribute it and/or 7 | # modify it under the terms of the GNU Lesser General Public 8 | # License as published by the Free Software Foundation; either 9 | # version 2.1 of the licence, or (at your option) any later version. 10 | # 11 | # This program is distributed in the hope that it will be useful, 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 14 | # Lesser General Public License for more details. 15 | # 16 | # You should have received a copy of the GNU Lesser General Public 17 | # License along with this program; if not, see . 18 | # 19 | # Authors: Alberto Ruiz 20 | # Oliver Gutiérrez 21 | 22 | import os 23 | import stat 24 | import logging 25 | import shutil 26 | import json 27 | 28 | from fleetcommanderclient.adapters.base import BaseAdapter 29 | 30 | 31 | class FirefoxBookmarksAdapter(BaseAdapter): 32 | """ 33 | Firefox bookmarks configuration adapter class 34 | """ 35 | 36 | # Namespace this config adapter handles 37 | NAMESPACE = "org.mozilla.firefox.Bookmarks" 38 | POLICIES_FILENAME = "policies.json" 39 | 40 | def __init__(self, policies_path): 41 | self.policies_path = policies_path 42 | 43 | def process_config_data(self, config_data, cache_path): 44 | """ 45 | Process configuration data and save cache files to be deployed. 46 | This method needs to be defined by each configuration adapter. 47 | """ 48 | # Prepare data 49 | bookmarks = [] 50 | for item in config_data: 51 | if "key" in item and "value" in item: 52 | bookmarks.append(item["value"]) 53 | policies_data = {"policies": {"Bookmarks": bookmarks}} 54 | # Write preferences data 55 | path = os.path.join(cache_path, "fleet-commander") 56 | logging.debug("Writing preferences data to %s", path) 57 | with open(path, "w") as fd: 58 | fd.write(json.dumps(policies_data)) 59 | fd.close() 60 | 61 | def deploy_files(self, cache_path, uid): 62 | """ 63 | Copy cached policies file to policies directory 64 | This method will be called by privileged process 65 | """ 66 | cached_file_path = os.path.join(cache_path, "fleet-commander") 67 | if os.path.isfile(cached_file_path): 68 | logging.debug("Deploying preferences at %s.", cached_file_path) 69 | directory = self.policies_path.format(uid) 70 | path = os.path.join(directory, self.POLICIES_FILENAME) 71 | # Remove previous preferences file 72 | logging.debug("Removing previous policies file %s", path) 73 | try: 74 | os.remove(path) 75 | except Exception as e: 76 | logging.debug("Failed to remove previous policies file %s: %s", path, e) 77 | # Create directory 78 | try: 79 | os.makedirs(directory) 80 | except Exception: 81 | pass 82 | # Deploy new policies file 83 | logging.debug("Copying policies file at %s to %s", cached_file_path, path) 84 | shutil.copyfile(cached_file_path, path) 85 | # Change permissions and ownership 86 | os.chown(path, uid, -1) 87 | os.chmod(path, stat.S_IREAD) 88 | else: 89 | logging.debug("No policies file at %s. Ignoring.", cached_file_path) 90 | -------------------------------------------------------------------------------- /src/fleetcommanderclient/adapters/firefox.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # vi:ts=4 sw=4 sts=4 3 | 4 | # Copyright (C) 2017 Red Hat, Inc. 5 | # 6 | # This program is free software; you can redistribute it and/or 7 | # modify it under the terms of the GNU Lesser General Public 8 | # License as published by the Free Software Foundation; either 9 | # version 2.1 of the licence, or (at your option) any later version. 10 | # 11 | # This program is distributed in the hope that it will be useful, 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 14 | # Lesser General Public License for more details. 15 | # 16 | # You should have received a copy of the GNU Lesser General Public 17 | # License along with this program; if not, see . 18 | # 19 | # Authors: Alberto Ruiz 20 | # Oliver Gutiérrez 21 | 22 | import os 23 | import stat 24 | import logging 25 | import shutil 26 | import json 27 | 28 | from fleetcommanderclient.adapters.base import BaseAdapter 29 | 30 | 31 | class FirefoxAdapter(BaseAdapter): 32 | """ 33 | Firefox configuration adapter class 34 | """ 35 | 36 | # Namespace this config adapter handles 37 | NAMESPACE = "org.mozilla.firefox" 38 | 39 | PREFS_FILENAME = "fleet-commander-{}" 40 | PREF_TEMPLATE = 'pref("{}", {});' 41 | 42 | def __init__(self, prefs_path): 43 | self.prefs_path = prefs_path 44 | 45 | def process_config_data(self, config_data, cache_path): 46 | """ 47 | Process configuration data and save cache files to be deployed. 48 | This method needs to be defined by each configuration adapter. 49 | """ 50 | # Prepare data 51 | preferences = [] 52 | for item in config_data: 53 | if "key" in item and "value" in item: 54 | # TODO: Check for locked settings and use lockPref instead 55 | preferences.append( 56 | self.PREF_TEMPLATE.format(item["key"], json.dumps(item["value"])) 57 | ) 58 | # Write preferences data 59 | path = os.path.join(cache_path, "fleet-commander") 60 | logging.debug("Writing preferences data to %s", path) 61 | with open(path, "w") as fd: 62 | fd.write("\n".join(preferences)) 63 | fd.close() 64 | 65 | def deploy_files(self, cache_path, uid): 66 | """ 67 | Copy cached policies file to policies directory 68 | This method will be called by privileged process 69 | """ 70 | cached_file_path = os.path.join(cache_path, "fleet-commander") 71 | if os.path.isfile(cached_file_path): 72 | logging.debug("Deploying preferences at %s.", cached_file_path) 73 | filename = self.PREFS_FILENAME.format(uid) 74 | path = os.path.join(self.prefs_path, filename) 75 | # Remove previous preferences file 76 | logging.debug("Removing previous preferences file %s", path) 77 | try: 78 | os.remove(path) 79 | except Exception as e: 80 | logging.debug( 81 | "Failed to remove previous preferences file %s: %s", path, e 82 | ) 83 | 84 | # Deploy new preferences file 85 | logging.debug( 86 | "Copying preferences file at %s to %s", cached_file_path, path 87 | ) 88 | shutil.copyfile(cached_file_path, path) 89 | # Change permissions and ownership 90 | os.chown(path, uid, -1) 91 | os.chmod(path, stat.S_IREAD) 92 | else: 93 | logging.debug("No preferences file at %s. Ignoring.", cached_file_path) 94 | -------------------------------------------------------------------------------- /tests/06_configadapter_chromium.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | # vi:ts=2 sw=2 sts=2 4 | 5 | # Copyright (C) 2017 Red Hat, Inc. 6 | # 7 | # This program is free software; you can redistribute it and/or 8 | # modify it under the terms of the GNU Lesser General Public 9 | # License as published by the Free Software Foundation; either 10 | # version 2.1 of the licence, or (at your option) any later version. 11 | # 12 | # This program is distributed in the hope that it will be useful, 13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 15 | # Lesser General Public License for more details. 16 | # 17 | # You should have received a copy of the GNU Lesser General Public 18 | # License along with this program; if not, see . 19 | # 20 | # Authors: Alberto Ruiz 21 | # Oliver Gutiérrez 22 | 23 | import os 24 | import sys 25 | import tempfile 26 | import shutil 27 | import json 28 | import unittest 29 | import stat 30 | 31 | sys.path.append(os.path.join(os.environ["TOPSRCDIR"], "src")) 32 | 33 | import fleetcommanderclient.configadapters.chromium 34 | from fleetcommanderclient.configadapters.chromium import ChromiumConfigAdapter 35 | 36 | 37 | def universal_function(*args, **kwargs): 38 | pass 39 | 40 | 41 | # Monkey patch chown function in os module for chromium config adapter 42 | fleetcommanderclient.configadapters.chromium.os.chown = universal_function 43 | 44 | 45 | class TestChromiumConfigAdapter(unittest.TestCase): 46 | TEST_UID = os.getuid() 47 | 48 | TEST_DATA = [ 49 | {"value": True, "key": "ShowHomeButton"}, 50 | {"value": True, "key": "BookmarkBarEnabled"}, 51 | ] 52 | 53 | def setUp(self): 54 | self.test_directory = tempfile.mkdtemp(prefix="fc-client-chromium-test") 55 | self.policies_path = os.path.join(self.test_directory, "managed") 56 | self.policies_file_path = os.path.join( 57 | self.policies_path, ChromiumConfigAdapter.POLICIES_FILENAME % self.TEST_UID 58 | ) 59 | self.ca = ChromiumConfigAdapter(self.policies_path) 60 | 61 | def tearDown(self): 62 | # Remove test directory 63 | shutil.rmtree(self.test_directory) 64 | 65 | def test_00_bootstrap(self): 66 | # Run bootstrap with no directory created should continue 67 | self.ca.bootstrap(self.TEST_UID) 68 | # Run bootstrap with existing directories 69 | os.makedirs(self.policies_path) 70 | with open(self.policies_file_path, "w") as fd: 71 | fd.write("POLICIES") 72 | fd.close() 73 | self.assertTrue(os.path.isdir(self.policies_path)) 74 | self.assertTrue(os.path.exists(self.policies_file_path)) 75 | self.ca.bootstrap(self.TEST_UID) 76 | # Check file has been removed 77 | self.assertFalse(os.path.exists(self.policies_file_path)) 78 | self.assertTrue(os.path.isdir(self.policies_path)) 79 | 80 | def test_01_update(self): 81 | self.ca.bootstrap(self.TEST_UID) 82 | self.ca.update(self.TEST_UID, self.TEST_DATA) 83 | # Check file has been written 84 | self.assertTrue(os.path.exists(self.policies_file_path)) 85 | # Change file mod because test user haven't root privilege 86 | os.chmod(self.policies_file_path, stat.S_IRUSR) 87 | # Read file 88 | with open(self.policies_file_path, "r") as fd: 89 | data = json.loads(fd.read()) 90 | fd.close() 91 | # Check file contents are ok 92 | for item in self.TEST_DATA: 93 | self.assertTrue(item["key"] in data) 94 | self.assertEqual(item["value"], data[item["key"]]) 95 | 96 | 97 | if __name__ == "__main__": 98 | unittest.main() 99 | -------------------------------------------------------------------------------- /src/fleetcommanderclient/adapters/base.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # vi:ts=4 sw=4 sts=4 3 | 4 | # Copyright (C) 2019 Red Hat, Inc. 5 | # 6 | # This program is free software; you can redistribute it and/or 7 | # modify it under the terms of the GNU Lesser General Public 8 | # License as published by the Free Software Foundation; either 9 | # version 2.1 of the licence, or (at your option) any later version. 10 | # 11 | # This program is distributed in the hope that it will be useful, 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 14 | # Lesser General Public License for more details. 15 | # 16 | # You should have received a copy of the GNU Lesser General Public 17 | # License along with this program; if not, see . 18 | # 19 | # Authors: Alberto Ruiz 20 | # Oliver Gutiérrez 21 | 22 | 23 | import os 24 | import pwd 25 | import shutil 26 | import logging 27 | 28 | 29 | class BaseAdapter: 30 | """ 31 | Base configuration adapter class 32 | """ 33 | 34 | # Namespace this config adapter handles 35 | NAMESPACE = None 36 | 37 | # Variable for setting cache path for testing 38 | _TEST_CACHE_PATH = None 39 | 40 | def _get_cache_path(self, uid=None): 41 | # Use test cache path while testing 42 | if self._TEST_CACHE_PATH is not None: 43 | return os.path.join(self._TEST_CACHE_PATH, self.NAMESPACE) 44 | 45 | if uid is None: 46 | # Use current user home cache directory 47 | return os.path.join( 48 | os.path.expanduser("~"), ".cache/fleet-commander", self.NAMESPACE 49 | ) 50 | # Get user directory from password database 51 | homedir = getattr(pwd.getpwuid(uid), "pw_dir") 52 | return os.path.join(homedir, ".cache/fleet-commander/", self.NAMESPACE) 53 | 54 | def cleanup_cache(self, namespace_cache_path=None): 55 | """ 56 | Removes all files under cache for this adapter namespace 57 | """ 58 | if namespace_cache_path is None: 59 | namespace_cache_path = self._get_cache_path() 60 | logging.debug("Cleaning up cache path %s", namespace_cache_path) 61 | if os.path.exists(namespace_cache_path): 62 | shutil.rmtree(namespace_cache_path) 63 | 64 | def generate_config(self, config_data): 65 | """ 66 | Prepare files to be deployed 67 | """ 68 | namespace_cache_path = self._get_cache_path() 69 | # Cleaning up cache path 70 | self.cleanup_cache(namespace_cache_path) 71 | # Create namespace cache path 72 | logging.debug("Creating cache path %s", namespace_cache_path) 73 | os.makedirs(namespace_cache_path) 74 | logging.debug("Processing data configuration for namespace %s", self.NAMESPACE) 75 | self.process_config_data(config_data, namespace_cache_path) 76 | 77 | def deploy(self, uid): 78 | """ 79 | Deploy configuration method 80 | """ 81 | namespace_cache_path = self._get_cache_path(uid) 82 | self.deploy_files(namespace_cache_path, uid) 83 | 84 | def process_config_data(self, config_data, cache_path): 85 | """ 86 | Process configuration data and save cache files to be deployed. 87 | This method needs to be defined by each configuration adapter. 88 | """ 89 | raise NotImplementedError("You must implement generate_config_data method") 90 | 91 | def deploy_files(self, cache_path, uid): 92 | """ 93 | File deployment method to be defined by each configuration adapter. 94 | This method will be called by privileged process 95 | """ 96 | raise NotImplementedError("You must implement deploy_files method") 97 | -------------------------------------------------------------------------------- /configure.ac: -------------------------------------------------------------------------------- 1 | AC_INIT(fleet-commander-client, 0.16.0, aruiz@redhat.com) 2 | AC_COPYRIGHT([Copyright 2014,2015,2016,2017,2018,2019 Red Hat, Inc.]) 3 | 4 | AC_PREREQ(2.64) 5 | AM_INIT_AUTOMAKE([no-dist-gzip dist-xz]) 6 | AM_MAINTAINER_MODE 7 | AC_CONFIG_MACRO_DIR([m4]) 8 | m4_ifdef([AM_SILENT_RULES],[AM_SILENT_RULES([yes])]) 9 | 10 | AC_PATH_PROG([RUNUSER], [runuser]) 11 | AC_PATH_PROG([MKDIR], [mkdir]) 12 | 13 | if test x$RUNUSER = x ; then 14 | AC_MSG_ERROR([Could not find runuser]) 15 | fi 16 | 17 | PKG_PROG_PKG_CONFIG 18 | 19 | AC_ARG_WITH([systemdsystemunitdir], 20 | [AS_HELP_STRING([--with-systemdsystemunitdir=DIR], [Directory for systemd system service files])],, 21 | [with_systemdsystemunitdir=auto]) 22 | AS_IF([test "x$with_systemdsystemunitdir" = "xyes" -o "x$with_systemdsystemunitdir" = "xauto"], [ 23 | def_systemdsystemunitdir=$($PKG_CONFIG --variable=systemdsystemunitdir systemd) 24 | 25 | AS_IF([test "x$def_systemdsystemunitdir" = "x"], 26 | [AS_IF([test "x$with_systemdsystemunitdir" = "xyes"], 27 | [AC_MSG_ERROR([systemd support requested but pkg-config unable to query systemd package])]) 28 | with_systemdsystemunitdir=no], 29 | [with_systemdsystemunitdir="$def_systemdsystemunitdir"])]) 30 | AS_IF([test "x$with_systemdsystemunitdir" != "xno"], 31 | [AC_SUBST([systemdsystemunitdir], [$with_systemdsystemunitdir])]) 32 | AM_CONDITIONAL([HAVE_SYSTEMD], [test "x$with_systemdsystemunitdir" != "xno"]) 33 | 34 | if test "x$with_systemdsystemunitdir" = "xno"; then 35 | AC_MSG_ERROR([systemd support is mandatory]) 36 | fi 37 | 38 | AC_ARG_WITH([systemduserunitdir], 39 | [AS_HELP_STRING([--with-systemduserunitdir=DIR], [Directory for systemd user service files])],, 40 | [with_systemduserunitdir=auto]) 41 | AS_IF([test "x$with_systemduserunitdir" = "xyes" -o "x$with_systemduserunitdir" = "xauto"], [ 42 | def_systemduserunitdir=$($PKG_CONFIG --variable=systemduserunitdir systemd) 43 | 44 | AS_IF([test "x$def_systemduserunitdir" = "x"], 45 | [AS_IF([test "x$with_systemduserunitdir" = "xyes"], 46 | [AC_MSG_ERROR([systemd support requested but pkg-config unable to query systemd package])]) 47 | with_systemduserunitdir=no], 48 | [with_systemduserunitdir="$def_systemduserunitdir"])]) 49 | AS_IF([test "x$with_systemduserunitdir" != "xno"], 50 | [AC_SUBST([systemduserunitdir], [$with_systemduserunitdir])]) 51 | AM_CONDITIONAL([HAVE_SYSTEMD], [test "x$with_systemduserunitdir" != "xno"]) 52 | 53 | if test "x$with_systemduserunitdir" = "xno"; then 54 | AC_MSG_ERROR([systemd support is mandatory]) 55 | fi 56 | 57 | 58 | ################ 59 | # Dependencies # 60 | ################ 61 | 62 | AM_PATH_PYTHON([3],, [:]) 63 | #AC_PYTHON_MODULE([dbus], [mandatory]) 64 | AC_PYTHON_MODULE([gi], [mandatory]) 65 | # AC_PYTHON_MODULE([ldap], [mandatory]) 66 | # AC_PYTHON_MODULE([samba], [mandatory]) 67 | AC_PYTHON_MODULE([dbusmock]) 68 | AC_PYTHON_MODULE([mock]) 69 | 70 | # libexecdir expansion for .desktop file 71 | # TODO: Make xdgconfigdir parametric 72 | privlibexecdir='${libexecdir}' 73 | xdgconfigdir='${sysconfdir}'/xdg 74 | clientstatedir='${localstatedir}'/lib/fleet-commander-client 75 | fcclientdir='${datarootdir}'/fleet-commander-client 76 | fcpythondir='${datarootdir}'/fleet-commander-client/python 77 | 78 | AC_SUBST(privlibexecdir) 79 | AC_SUBST(xdgconfigdir) 80 | AC_SUBST(clientstatedir) 81 | AC_SUBST(fcclientdir) 82 | AC_SUBST(fcpythondir) 83 | 84 | AS_AC_EXPAND(XDGCONFIGDIR, "$xdgconfigdir") 85 | AS_AC_EXPAND(PRIVLIBEXECDIR, "$privlibexecdir") 86 | AS_AC_EXPAND(CLIENTSTATEDIR, "$clientstatedir") 87 | AS_AC_EXPAND(FCCLIENTDIR, "$fcclientdir") 88 | AS_AC_EXPAND(FCPYTHONDIR, "$fcpythondir") 89 | 90 | AC_SUBST(SYSTEMUNITDIR) 91 | 92 | AC_OUTPUT([ 93 | Makefile 94 | data/Makefile 95 | tests/Makefile 96 | src/Makefile 97 | data/fleet-commander-client.service 98 | data/fleet-commander-clientad.service 99 | data/fleet-commander-adretriever.service 100 | data/org.freedesktop.FleetCommanderClient.service 101 | data/org.freedesktop.FleetCommanderClientAD.service 102 | ]) 103 | -------------------------------------------------------------------------------- /tests/03_configadapter_goa.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | # vi:ts=2 sw=2 sts=2 4 | 5 | # Copyright (C) 2017 Red Hat, Inc. 6 | # 7 | # This program is free software; you can redistribute it and/or 8 | # modify it under the terms of the GNU Lesser General Public 9 | # License as published by the Free Software Foundation; either 10 | # version 2.1 of the licence, or (at your option) any later version. 11 | # 12 | # This program is distributed in the hope that it will be useful, 13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 15 | # Lesser General Public License for more details. 16 | # 17 | # You should have received a copy of the GNU Lesser General Public 18 | # License along with this program; if not, see . 19 | # 20 | # Authors: Alberto Ruiz 21 | # Oliver Gutiérrez 22 | 23 | import os 24 | import sys 25 | import tempfile 26 | import shutil 27 | import unittest 28 | 29 | from gi.repository import GLib 30 | 31 | sys.path.append(os.path.join(os.environ["TOPSRCDIR"], "src")) 32 | 33 | 34 | from fleetcommanderclient.configadapters.goa import GOAConfigAdapter 35 | 36 | 37 | class TestGOAConfigAdapter(unittest.TestCase): 38 | 39 | TEST_UID = 55555 40 | 41 | TEST_DATA = { 42 | "Template account_fc_1490729747_0": { 43 | "FilesEnabled": True, 44 | "PhotosEnabled": False, 45 | "ContactsEnabled": False, 46 | "CalendarEnabled": True, 47 | "Provider": "google", 48 | "DocumentsEnabled": False, 49 | "PrintersEnabled": True, 50 | "MailEnabled": True, 51 | }, 52 | "Template account_fc_1490729585_0": { 53 | "PhotosEnabled": False, 54 | "Provider": "facebook", 55 | "MapsEnabled": False, 56 | }, 57 | } 58 | 59 | def setUp(self): 60 | self.test_directory = tempfile.mkdtemp(prefix="fc-client-goa-test") 61 | self.ca = GOAConfigAdapter(self.test_directory) 62 | 63 | def tearDown(self): 64 | # Remove test directory 65 | shutil.rmtree(self.test_directory) 66 | 67 | def test_00_bootstrap(self): 68 | dirpath = os.path.join(self.test_directory, str(self.TEST_UID)) 69 | # Run bootstrap with no directory created should continue and warn 70 | self.ca.bootstrap(self.TEST_UID) 71 | # Run bootstrap with a existing directory 72 | os.makedirs(dirpath) 73 | self.assertTrue(os.path.exists(dirpath)) 74 | self.ca.bootstrap(self.TEST_UID) 75 | # Check directory has been removed 76 | self.assertFalse(os.path.exists(dirpath)) 77 | 78 | def test_01_update(self): 79 | self.ca.bootstrap(self.TEST_UID) 80 | self.ca.update(self.TEST_UID, self.TEST_DATA) 81 | keyfile_path = os.path.join( 82 | self.test_directory, str(self.TEST_UID), self.ca.FC_ACCOUNTS_FILE 83 | ) 84 | # Check keyfile has been written 85 | self.assertTrue(os.path.exists(keyfile_path)) 86 | # Read keyfile 87 | keyfile = GLib.KeyFile.new() 88 | keyfile.load_from_file(keyfile_path, GLib.KeyFileFlags.NONE) 89 | 90 | # Check section list 91 | accounts = list(self.TEST_DATA.keys()) 92 | accounts_keyfile = keyfile.get_groups()[0] 93 | accounts_keyfile.sort() 94 | self.assertEqual(sorted(accounts), accounts_keyfile) 95 | 96 | # Check all sections 97 | for account, accountdata in self.TEST_DATA.items(): 98 | # Check all keys and values 99 | for key, value in accountdata.items(): 100 | if isinstance(value, bool): 101 | value_keyfile = keyfile.get_boolean(account, key) 102 | else: 103 | value_keyfile = keyfile.get_string(account, key) 104 | self.assertEqual(value, value_keyfile) 105 | 106 | 107 | if __name__ == "__main__": 108 | unittest.main() 109 | -------------------------------------------------------------------------------- /src/fleetcommanderclient/settingscompiler.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # vi:ts=4 sw=4 sts=4 3 | 4 | # Copyright (C) 2017 Red Hat, Inc. 5 | # 6 | # This program is free software; you can redistribute it and/or 7 | # modify it under the terms of the GNU Lesser General Public 8 | # License as published by the Free Software Foundation; either 9 | # version 2.1 of the licence, or (at your option) any later version. 10 | # 11 | # This program is distributed in the hope that it will be useful, 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 14 | # Lesser General Public License for more details. 15 | # 16 | # You should have received a copy of the GNU Lesser General Public 17 | # License along with this program; if not, see . 18 | # 19 | # Authors: Alberto Ruiz 20 | # Oliver Gutiérrez 21 | 22 | import os 23 | import logging 24 | import json 25 | 26 | from fleetcommanderclient import mergers 27 | 28 | 29 | class SettingsCompiler: 30 | """ 31 | Profile settings compiler class 32 | 33 | Generates final profile settings merging data from files in a given path 34 | """ 35 | 36 | def __init__(self, path): 37 | self.path = path 38 | 39 | # Initialize data mergers 40 | self.mergers = { 41 | "org.gnome.gsettings": mergers.GSettingsMerger(), 42 | "org.libreoffice.registry": mergers.LibreOfficeMerger(), 43 | "org.gnome.online-accounts": mergers.GOAMerger(), 44 | "org.mozilla.firefox": mergers.FirefoxMerger(), 45 | "org.freedesktop.NetworkManager": mergers.NetworkManagerMerger(), 46 | } 47 | 48 | def get_ordered_file_names(self): 49 | """ 50 | Get file name list from path given at class initialization 51 | """ 52 | filenames = os.listdir(self.path) 53 | filenames.sort() 54 | return filenames 55 | 56 | def read_profile_settings(self, filename): 57 | """ 58 | Read profile settings from given file 59 | """ 60 | filepath = os.path.join(self.path, filename) 61 | try: 62 | with open(filepath, "r") as fd: 63 | contents = fd.read() 64 | data = json.loads(contents) 65 | fd.close() 66 | return data 67 | except Exception as e: 68 | logging.error( 69 | "ProfileGenerator: Ignoring profile data from %s: %s", filepath, e 70 | ) 71 | return {} 72 | 73 | def merge_profile_settings(self, old, new): 74 | """ 75 | Merge two profiles overwriting previous values with new ones 76 | """ 77 | for namespace in new.keys(): 78 | # Check for merger 79 | if namespace in self.mergers: 80 | if namespace not in old.keys(): 81 | old[namespace] = new[namespace] 82 | else: 83 | old[namespace] = self.mergers[namespace].merge( 84 | old[namespace], new[namespace] 85 | ) 86 | else: 87 | old[namespace] = new[namespace] 88 | return old 89 | 90 | def compile_settings(self): 91 | """ 92 | Generate final settings 93 | """ 94 | filenames = self.get_ordered_file_names() 95 | profile_settings = {} 96 | for filename in filenames: 97 | data = self.read_profile_settings(filename) 98 | profile_settings = self.merge_profile_settings(profile_settings, data) 99 | 100 | # FIXME: Right now merging libreoffice config data into gsettings 101 | # because both use the same configuration adapter. 102 | # We should change the config adapter interface to allow 103 | # multiple namespaces for the same config adapter. 104 | 105 | if "org.libreoffice.registry" in profile_settings.keys(): 106 | libreoffice_data = { 107 | "org.gnome.gsettings": profile_settings["org.libreoffice.registry"] 108 | } 109 | profile_settings = self.merge_profile_settings( 110 | profile_settings, libreoffice_data 111 | ) 112 | return profile_settings 113 | -------------------------------------------------------------------------------- /tests/11_adapter_chromium.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | # vi:ts=2 sw=2 sts=2 4 | 5 | # Copyright (C) 2019 Red Hat, Inc. 6 | # 7 | # This program is free software; you can redistribute it and/or 8 | # modify it under the terms of the GNU Lesser General Public 9 | # License as published by the Free Software Foundation; either 10 | # version 2.1 of the licence, or (at your option) any later version. 11 | # 12 | # This program is distributed in the hope that it will be useful, 13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 15 | # Lesser General Public License for more details. 16 | # 17 | # You should have received a copy of the GNU Lesser General Public 18 | # License along with this program; if not, see . 19 | # 20 | # Authors: Alberto Ruiz 21 | # Oliver Gutiérrez 22 | 23 | import os 24 | import sys 25 | import logging 26 | import tempfile 27 | import shutil 28 | import json 29 | import unittest 30 | 31 | sys.path.append(os.path.join(os.environ["TOPSRCDIR"], "src")) 32 | 33 | import fleetcommanderclient.adapters.chromium 34 | from fleetcommanderclient.adapters.chromium import ChromiumAdapter 35 | 36 | 37 | def universal_function(*args, **kwargs): 38 | pass 39 | 40 | 41 | # Monkey patch chown function in os module for chromium config adapter 42 | fleetcommanderclient.adapters.chromium.os.chown = universal_function 43 | 44 | 45 | # Set log level to debug 46 | logging.basicConfig(level=logging.DEBUG) 47 | 48 | 49 | class TestChromiumAdapter(unittest.TestCase): 50 | 51 | TEST_UID = 55555 52 | 53 | TEST_DATA = [ 54 | {"value": True, "key": "ShowHomeButton"}, 55 | {"value": True, "key": "BookmarkBarEnabled"}, 56 | ] 57 | 58 | TEST_PROCESSED_DATA = { 59 | "ShowHomeButton": True, 60 | "BookmarkBarEnabled": True, 61 | } 62 | 63 | def setUp(self): 64 | self.test_directory = tempfile.mkdtemp(prefix="fc-client-chromium-test") 65 | self.policies_path = os.path.join(self.test_directory, "managed") 66 | self.cache_path = os.path.join(self.test_directory, "cache") 67 | self.policies_file_path = os.path.join( 68 | self.policies_path, ChromiumAdapter.POLICIES_FILENAME.format(self.TEST_UID) 69 | ) 70 | self.ca = ChromiumAdapter(self.policies_path) 71 | self.ca._TEST_CACHE_PATH = self.cache_path 72 | 73 | def tearDown(self): 74 | # Remove test directory 75 | shutil.rmtree(self.test_directory) 76 | 77 | def test_00_generate_config(self): 78 | # Generate configuration 79 | self.ca.generate_config(self.TEST_DATA) 80 | # Check configuration file exists 81 | filepath = os.path.join( 82 | self.cache_path, self.ca.NAMESPACE, "fleet-commander.json" 83 | ) 84 | logging.debug("Checking %s exists", filepath) 85 | self.assertTrue(os.path.exists(filepath)) 86 | # Check configuration file contents 87 | with open(filepath, "r") as fd: 88 | data = json.loads(fd.read()) 89 | fd.close() 90 | self.assertEqual(data, self.TEST_PROCESSED_DATA) 91 | 92 | def test_01_deploy(self): 93 | # Generate config files in cache 94 | self.ca.generate_config(self.TEST_DATA) 95 | # Check deployment directory does not exist yet 96 | self.assertFalse(os.path.exists(self.policies_path)) 97 | # Execute deployment 98 | self.ca.deploy(self.TEST_UID) 99 | # Check file has been copied to policies path 100 | deployed_file_path = os.path.join( 101 | self.policies_path, ChromiumAdapter.POLICIES_FILENAME.format(self.TEST_UID) 102 | ) 103 | self.assertTrue(os.path.isfile(deployed_file_path)) 104 | # Check both files content is the same 105 | with open(deployed_file_path, "r") as fd: 106 | data1 = json.loads(fd.read()) 107 | fd.close() 108 | cached_file_path = os.path.join( 109 | self.cache_path, self.ca.NAMESPACE, "fleet-commander.json" 110 | ) 111 | with open(cached_file_path, "r") as fd: 112 | data2 = json.loads(fd.read()) 113 | fd.close() 114 | self.assertEqual(data1, data2) 115 | 116 | 117 | if __name__ == "__main__": 118 | unittest.main() 119 | -------------------------------------------------------------------------------- /src/fleetcommanderclient/adapters/chromium.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # vi:ts=4 sw=4 sts=4 3 | 4 | # Copyright (C) 2019 Red Hat, Inc. 5 | # 6 | # This program is free software; you can redistribute it and/or 7 | # modify it under the terms of the GNU Lesser General Public 8 | # License as published by the Free Software Foundation; either 9 | # version 2.1 of the licence, or (at your option) any later version. 10 | # 11 | # This program is distributed in the hope that it will be useful, 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 14 | # Lesser General Public License for more details. 15 | # 16 | # You should have received a copy of the GNU Lesser General Public 17 | # License along with this program; if not, see . 18 | # 19 | # Authors: Alberto Ruiz 20 | # Oliver Gutiérrez 21 | 22 | import os 23 | import stat 24 | import logging 25 | import shutil 26 | import json 27 | 28 | from fleetcommanderclient.adapters.base import BaseAdapter 29 | 30 | 31 | class ChromiumAdapter(BaseAdapter): 32 | """ 33 | Chromium configuration adapter class 34 | """ 35 | 36 | # Namespace this config adapter handles 37 | NAMESPACE = "org.chromium.Policies" 38 | 39 | POLICIES_FILENAME = "fleet-commander-{}.json" 40 | 41 | def __init__(self, policies_path): 42 | self.policies_path = policies_path 43 | 44 | def _get_policies_file_path(self, uid): 45 | filename = self.POLICIES_FILENAME.format(uid) 46 | return os.path.join(self.policies_path, filename) 47 | 48 | def process_config_data(self, config_data, cache_path): 49 | """ 50 | Process configuration data and save cache files to be deployed. 51 | This method needs to be defined by each configuration adapter. 52 | """ 53 | # Prepare data 54 | policies = {} 55 | for item in config_data: 56 | if "key" in item and "value" in item: 57 | policies[item["key"]] = item["value"] 58 | # Write policies data 59 | path = os.path.join(cache_path, "fleet-commander.json") 60 | logging.debug("Writing policies data to %s", path) 61 | with open(path, "w") as fd: 62 | fd.write(json.dumps(policies)) 63 | fd.close() 64 | 65 | def deploy_files(self, cache_path, uid): 66 | """ 67 | Copy cached policies file to policies directory 68 | This method will be called by privileged process 69 | """ 70 | 71 | cached_file_path = os.path.join(cache_path, "fleet-commander.json") 72 | 73 | if os.path.isfile(cached_file_path): 74 | logging.debug("Deploying policies at %s.", cached_file_path) 75 | # Create policies path if does not exist 76 | if not os.path.exists(self.policies_path): 77 | logging.debug("Creating policies directory %s", self.policies_path) 78 | try: 79 | os.makedirs(self.policies_path) 80 | except Exception as e: 81 | logging.debug( 82 | "Failed to create policies directory %s: %s", 83 | self.policies_path, 84 | e, 85 | ) 86 | 87 | # Delete any previous file at managed profiles 88 | path = os.path.join(self.policies_path, self.POLICIES_FILENAME.format(uid)) 89 | if os.path.isfile(path): 90 | logging.debug("Removing previous policies file %s", path) 91 | try: 92 | os.remove(path) 93 | except Exception as e: 94 | logging.debug("Failed to remove old policies file %s: %s", path, e) 95 | 96 | # Deploy new policies file 97 | logging.debug("Copying policies file at %s to %s", cached_file_path, path) 98 | shutil.copyfile(cached_file_path, path) 99 | 100 | # Change permissions and ownership 101 | os.chown(path, uid, -1) 102 | os.chmod(path, stat.S_IREAD) 103 | else: 104 | logging.debug("No policies file at %s. Ignoring.", cached_file_path) 105 | 106 | 107 | class ChromeAdapter(ChromiumAdapter): 108 | """ 109 | Chrome config adapter 110 | """ 111 | 112 | NAMESPACE = "org.google.chrome.Policies" 113 | -------------------------------------------------------------------------------- /src/fleetcommanderclient/adapters/goa.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # vi:ts=4 sw=4 sts=4 3 | 4 | # Copyright (C) 2019 Red Hat, Inc. 5 | # 6 | # This program is free software; you can redistribute it and/or 7 | # modify it under the terms of the GNU Lesser General Public 8 | # License as published by the Free Software Foundation; either 9 | # version 2.1 of the licence, or (at your option) any later version. 10 | # 11 | # This program is distributed in the hope that it will be useful, 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 14 | # Lesser General Public License for more details. 15 | # 16 | # You should have received a copy of the GNU Lesser General Public 17 | # License along with this program; if not, see . 18 | # 19 | # Authors: Alberto Ruiz 20 | # Oliver Gutiérrez 21 | 22 | import os 23 | import stat 24 | import logging 25 | import shutil 26 | 27 | from gi.repository import GLib 28 | 29 | from fleetcommanderclient.adapters.base import BaseAdapter 30 | 31 | 32 | class GOAAdapter(BaseAdapter): 33 | """ 34 | GOA configuration adapter class 35 | """ 36 | 37 | # Namespace this config adapter handles 38 | NAMESPACE = "org.gnome.online-accounts" 39 | 40 | ACCOUNTS_FILE = "fleet-commander-accounts.conf" 41 | 42 | def __init__(self, goa_runtime_path): 43 | self.goa_runtime_path = goa_runtime_path 44 | 45 | def process_config_data(self, config_data, cache_path): 46 | """ 47 | Process configuration data and save cache files to be deployed. 48 | This method needs to be defined by each configuration adapter. 49 | """ 50 | # Prepare data for saving it in keyfile 51 | logging.debug("Preparing GOA data for saving to keyfile") 52 | keyfile = GLib.KeyFile.new() 53 | for account, accountdata in config_data.items(): 54 | for key, value in accountdata.items(): 55 | if isinstance(value, bool): 56 | keyfile.set_boolean(account, key, value) 57 | else: 58 | keyfile.set_string(account, key, value) 59 | 60 | # Save config file 61 | keyfile_path = os.path.join(cache_path, self.ACCOUNTS_FILE) 62 | logging.debug('Saving GOA keyfile to "%s"', keyfile_path) 63 | try: 64 | keyfile.save_to_file(keyfile_path) 65 | except Exception as e: 66 | logging.error("Error saving GOA keyfile at %s: %s", keyfile_path, e) 67 | return 68 | 69 | def deploy_files(self, cache_path, uid): 70 | """ 71 | Copy cached policies file to policies directory 72 | This method will be called by privileged process 73 | """ 74 | cached_file_path = os.path.join(cache_path, self.ACCOUNTS_FILE) 75 | 76 | if os.path.isfile(cached_file_path): 77 | logging.debug("Deploying GOA accounts from %s", cached_file_path) 78 | 79 | # Remove previous GOA files 80 | runtime_path = os.path.join(self.goa_runtime_path, str(uid)) 81 | logging.debug("Removing GOA runtime path %s", runtime_path) 82 | try: 83 | shutil.rmtree(runtime_path) 84 | except Exception as e: 85 | logging.warning( 86 | "Error removing GOA runtime path %s: %s", runtime_path, e 87 | ) 88 | 89 | # Create runtime path 90 | logging.debug("Creating GOA runtime path %s", runtime_path) 91 | try: 92 | os.makedirs(runtime_path) 93 | except Exception as e: 94 | logging.error("Error creating GOA runtime path %s: %s", runtime_path, e) 95 | return 96 | 97 | # Copy file from cache to runtime path 98 | deploy_file_path = os.path.join(runtime_path, self.ACCOUNTS_FILE) 99 | shutil.copyfile(cached_file_path, deploy_file_path) 100 | 101 | # Change permissions and ownership for accounts file 102 | os.chown(deploy_file_path, uid, -1) 103 | os.chmod(deploy_file_path, stat.S_IREAD) 104 | 105 | # Change permissions and ownership for GOA runtime directory 106 | os.chown(runtime_path, uid, -1) 107 | os.chmod(runtime_path, stat.S_IREAD | stat.S_IEXEC) 108 | else: 109 | logging.debug("GOA accounts file %s is not present", cached_file_path) 110 | -------------------------------------------------------------------------------- /tests/13_adapter_goa.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | # vi:ts=2 sw=2 sts=2 4 | 5 | # Copyright (C) 2019 Red Hat, Inc. 6 | # 7 | # This program is free software; you can redistribute it and/or 8 | # modify it under the terms of the GNU Lesser General Public 9 | # License as published by the Free Software Foundation; either 10 | # version 2.1 of the licence, or (at your option) any later version. 11 | # 12 | # This program is distributed in the hope that it will be useful, 13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 15 | # Lesser General Public License for more details. 16 | # 17 | # You should have received a copy of the GNU Lesser General Public 18 | # License along with this program; if not, see . 19 | # 20 | # Authors: Alberto Ruiz 21 | # Oliver Gutiérrez 22 | 23 | import os 24 | import sys 25 | import logging 26 | import tempfile 27 | import shutil 28 | import stat 29 | import unittest 30 | 31 | from gi.repository import GLib 32 | 33 | sys.path.append(os.path.join(os.environ["TOPSRCDIR"], "src")) 34 | 35 | import fleetcommanderclient.adapters.goa 36 | from fleetcommanderclient.adapters.goa import GOAAdapter 37 | 38 | 39 | def universal_function(*args, **kwargs): 40 | pass 41 | 42 | 43 | # Monkey patch chown function in os module for chromium config adapter 44 | fleetcommanderclient.adapters.goa.os.chown = universal_function 45 | 46 | 47 | # Set log level to debug 48 | logging.basicConfig(level=logging.DEBUG) 49 | 50 | 51 | class TestGOAAdapter(unittest.TestCase): 52 | 53 | TEST_UID = 55555 54 | 55 | TEST_DATA = { 56 | "Template account_fc_1490729747_0": { 57 | "FilesEnabled": True, 58 | "PhotosEnabled": False, 59 | "ContactsEnabled": False, 60 | "CalendarEnabled": True, 61 | "Provider": "google", 62 | "DocumentsEnabled": False, 63 | "PrintersEnabled": True, 64 | "MailEnabled": True, 65 | }, 66 | "Template account_fc_1490729585_0": { 67 | "PhotosEnabled": False, 68 | "Provider": "facebook", 69 | "MapsEnabled": False, 70 | }, 71 | } 72 | 73 | def setUp(self): 74 | self.test_directory = tempfile.mkdtemp(prefix="fc-client-goa-test") 75 | self.cache_path = os.path.join(self.test_directory, "cache") 76 | self.ca = GOAAdapter(self.test_directory) 77 | self.ca._TEST_CACHE_PATH = self.cache_path 78 | 79 | def tearDown(self): 80 | # Change permissions of directories to allow removal 81 | runtime_dir = os.path.join(self.test_directory, str(self.TEST_UID)) 82 | if os.path.exists(runtime_dir): 83 | os.chmod(runtime_dir, stat.S_IREAD | stat.S_IWRITE | stat.S_IEXEC) 84 | # Remove test directory 85 | shutil.rmtree(self.test_directory) 86 | 87 | def test_00_generate_config(self): 88 | # Generate configuration 89 | self.ca.generate_config(self.TEST_DATA) 90 | # Check configuration file exists 91 | filepath = os.path.join( 92 | self.cache_path, self.ca.NAMESPACE, self.ca.ACCOUNTS_FILE 93 | ) 94 | logging.debug("Checking %s exists", filepath) 95 | self.assertTrue(os.path.exists(filepath)) 96 | # Check configuration file contents 97 | 98 | # Read keyfile 99 | keyfile = GLib.KeyFile.new() 100 | keyfile.load_from_file(filepath, GLib.KeyFileFlags.NONE) 101 | 102 | # Check section list 103 | accounts = list(self.TEST_DATA.keys()) 104 | accounts_keyfile = keyfile.get_groups()[0] 105 | self.assertEqual(sorted(accounts), sorted(accounts_keyfile)) 106 | 107 | def test_01_deploy(self): 108 | # Generate config files in cache 109 | self.ca.generate_config(self.TEST_DATA) 110 | # Execute deployment 111 | self.ca.deploy(self.TEST_UID) 112 | # Check file has been copied to policies path 113 | deployed_file_path = os.path.join( 114 | self.test_directory, str(self.TEST_UID), self.ca.ACCOUNTS_FILE 115 | ) 116 | self.assertTrue(os.path.isfile(deployed_file_path)) 117 | # Check both files content is the same 118 | with open(deployed_file_path, "r") as fd: 119 | data1 = fd.read() 120 | fd.close() 121 | cached_file_path = os.path.join( 122 | self.cache_path, self.ca.NAMESPACE, self.ca.ACCOUNTS_FILE 123 | ) 124 | with open(cached_file_path, "r") as fd: 125 | data2 = fd.read() 126 | fd.close() 127 | self.assertEqual(data1, data2) 128 | 129 | 130 | if __name__ == "__main__": 131 | unittest.main() 132 | -------------------------------------------------------------------------------- /src/fleetcommanderclient/mergers.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # vi:ts=4 sw=4 sts=4 3 | 4 | # Copyright (C) 2017 Red Hat, Inc. 5 | # 6 | # This program is free software; you can redistribute it and/or 7 | # modify it under the terms of the GNU Lesser General Public 8 | # License as published by the Free Software Foundation; either 9 | # version 2.1 of the licence, or (at your option) any later version. 10 | # 11 | # This program is distributed in the hope that it will be useful, 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 14 | # Lesser General Public License for more details. 15 | # 16 | # You should have received a copy of the GNU Lesser General Public 17 | # License along with this program; if not, see . 18 | # 19 | # Authors: Alberto Ruiz 20 | # Oliver Gutiérrez 21 | 22 | # Python imports 23 | import logging 24 | 25 | 26 | class BaseMerger: 27 | """ 28 | Base merger class 29 | 30 | Default policy: Overwrite same key with new value, create new keys 31 | """ 32 | 33 | KEY_NAME = "key" 34 | 35 | def get_key(self, setting): 36 | """ 37 | Return setting key 38 | """ 39 | if self.KEY_NAME in setting: 40 | return setting[self.KEY_NAME] 41 | return None 42 | 43 | def merge(self, *args): 44 | """ 45 | Merge settings in the given order 46 | """ 47 | index = {} 48 | for settings in args: 49 | for setting in settings: 50 | key = self.get_key(setting) 51 | index[key] = setting 52 | return list(index.values()) 53 | 54 | 55 | class GSettingsMerger(BaseMerger): 56 | """ 57 | GSettings merger class 58 | 59 | Policy: Overwrite same key with new value, create new keys 60 | """ 61 | 62 | 63 | class LibreOfficeMerger(BaseMerger): 64 | """ 65 | LibreOffice setting merger class 66 | 67 | Policy: Overwrite same key with new value, create new keys 68 | """ 69 | 70 | 71 | class ChromiumMerger(BaseMerger): 72 | """ 73 | Chromium setting merger class 74 | 75 | Policy: Overwrite same key with new value, create new keys 76 | Except: ManagedBookmarks key: Merge contents 77 | """ 78 | 79 | def merge(self, *args): 80 | """ 81 | Merge settings in the given order 82 | """ 83 | index = {} 84 | bookmarks = [] 85 | for settings in args: 86 | for setting in settings: 87 | key = self.get_key(setting) 88 | if key == "ManagedBookmarks": 89 | bookmarks = self.merge_bookmarks(bookmarks, setting["value"]) 90 | setting = {self.KEY_NAME: key, "value": bookmarks} 91 | index[key] = setting 92 | return list(index.values()) 93 | 94 | def merge_bookmarks(self, a, b): 95 | for elem_b in b: 96 | logging.debug("Processing %s", elem_b) 97 | if "children" in elem_b: 98 | merged = False 99 | for elem_a in a: 100 | if elem_a["name"] == elem_b["name"] and "children" in elem_a: 101 | logging.debug("Processing children of %s", elem_b["name"]) 102 | elem_a["children"] = self.merge_bookmarks( 103 | elem_a["children"], elem_b["children"] 104 | ) 105 | merged = True 106 | break 107 | if not merged: 108 | a.append(elem_b) 109 | else: 110 | if elem_b not in a: 111 | a.append(elem_b) 112 | logging.debug("Returning %s", a) 113 | return a 114 | 115 | 116 | class FirefoxMerger(BaseMerger): 117 | """ 118 | Firefox setting merger class 119 | 120 | Policy: Overwrite same key with new value, create new keys 121 | """ 122 | 123 | 124 | class NetworkManagerMerger(BaseMerger): 125 | """ 126 | Network manager setting merger class 127 | 128 | Policy: Overwrite same key with new value, create new keys 129 | """ 130 | 131 | KEY_NAME = "uuid" 132 | 133 | 134 | class GOAMerger(BaseMerger): 135 | """ 136 | Policy: Overwrite same account with new one, create new accounts 137 | """ 138 | 139 | def get_key(self, setting): 140 | """ 141 | Return setting key 142 | """ 143 | return None 144 | 145 | def merge(self, *args): 146 | """ 147 | Merge settings in the given order 148 | """ 149 | accounts = {} 150 | for settings in args: 151 | for account_id in settings: 152 | accounts[account_id] = settings[account_id] 153 | return accounts 154 | -------------------------------------------------------------------------------- /tests/16_adapter_firefoxbookmarks.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | # vi:ts=2 sw=2 sts=2 4 | 5 | # Copyright (C) 2019 Red Hat, Inc. 6 | # 7 | # This program is free software; you can redistribute it and/or 8 | # modify it under the terms of the GNU Lesser General Public 9 | # License as published by the Free Software Foundation; either 10 | # version 2.1 of the licence, or (at your option) any later version. 11 | # 12 | # This program is distributed in the hope that it will be useful, 13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 15 | # Lesser General Public License for more details. 16 | # 17 | # You should have received a copy of the GNU Lesser General Public 18 | # License along with this program; if not, see . 19 | # 20 | # Authors: Alberto Ruiz 21 | # Oliver Gutiérrez 22 | 23 | import os 24 | import sys 25 | import logging 26 | import tempfile 27 | import shutil 28 | import json 29 | import unittest 30 | 31 | sys.path.append(os.path.join(os.environ["TOPSRCDIR"], "src")) 32 | 33 | import fleetcommanderclient.adapters.firefoxbookmarks 34 | from fleetcommanderclient.adapters.firefoxbookmarks import FirefoxBookmarksAdapter 35 | 36 | 37 | def universal_function(*args, **kwargs): 38 | pass 39 | 40 | 41 | # Monkey patch chown function in os module 42 | fleetcommanderclient.adapters.firefoxbookmarks.os.chown = universal_function 43 | 44 | 45 | # Set log level to debug 46 | logging.basicConfig(level=logging.DEBUG) 47 | 48 | PROFILE_FILE_CONTENTS = r"""{ 49 | "org.mozilla.firefox.Bookmarks": [ 50 | { 51 | "key": "blah", 52 | "value": { 53 | "Title": "Test bookmark", 54 | "URL": "https://example.com", 55 | "Favicon": "https://example.com/favicon.ico", 56 | "Placement": "toolbar", 57 | "Folder": "FolderName" 58 | } 59 | } 60 | ] 61 | }""" 62 | 63 | POLICIES_FILE_CONTENTS = { 64 | "policies": { 65 | "Bookmarks": [ 66 | { 67 | "Title": "Test bookmark", 68 | "URL": "https://example.com", 69 | "Favicon": "https://example.com/favicon.ico", 70 | "Placement": "toolbar", 71 | "Folder": "FolderName", 72 | } 73 | ] 74 | } 75 | } 76 | 77 | 78 | class TestFirefoxAdapter(unittest.TestCase): 79 | 80 | TEST_UID = 55555 81 | 82 | TEST_DATA = json.loads(PROFILE_FILE_CONTENTS)["org.mozilla.firefox.Bookmarks"] 83 | 84 | def setUp(self): 85 | self.test_directory = tempfile.mkdtemp(prefix="fc-client-firefoxbookmarks-test") 86 | policies_path_template = os.path.join(self.test_directory, "{}/firefox") 87 | self.policies_path = policies_path_template.format(self.TEST_UID) 88 | self.cache_path = os.path.join(self.test_directory, "cache") 89 | self.policies_file_path = os.path.join( 90 | self.policies_path, FirefoxBookmarksAdapter.POLICIES_FILENAME 91 | ) 92 | self.ca = FirefoxBookmarksAdapter(policies_path_template) 93 | self.ca._TEST_CACHE_PATH = self.cache_path 94 | 95 | def tearDown(self): 96 | # Remove test directory 97 | shutil.rmtree(self.test_directory) 98 | 99 | def test_00_generate_config(self): 100 | # Generate configuration 101 | self.ca.generate_config(self.TEST_DATA) 102 | # Check configuration file exists 103 | filepath = os.path.join(self.cache_path, self.ca.NAMESPACE, "fleet-commander") 104 | logging.debug("Checking %s exists", filepath) 105 | self.assertTrue(os.path.exists(filepath)) 106 | # Check configuration file contents 107 | with open(filepath, "r") as fd: 108 | data = json.loads(fd.read()) 109 | fd.close() 110 | self.assertEqual( 111 | json.dumps(POLICIES_FILE_CONTENTS, sort_keys=True), 112 | json.dumps(data, sort_keys=True), 113 | ) 114 | 115 | def test_01_deploy(self): 116 | # Generate config files in cache 117 | self.ca.generate_config(self.TEST_DATA) 118 | # Execute deployment 119 | self.ca.deploy(self.TEST_UID) 120 | # Check file has been copied to policies path 121 | self.assertTrue(os.path.isfile(self.policies_file_path)) 122 | # Check both files content is the same 123 | with open(self.policies_file_path, "r") as fd: 124 | data1 = fd.read() 125 | fd.close() 126 | cached_file_path = os.path.join( 127 | self.cache_path, self.ca.NAMESPACE, "fleet-commander" 128 | ) 129 | with open(cached_file_path, "r") as fd: 130 | data2 = fd.read() 131 | fd.close() 132 | self.assertEqual(data1, data2) 133 | 134 | 135 | if __name__ == "__main__": 136 | unittest.main() 137 | -------------------------------------------------------------------------------- /src/fleetcommanderclient/fcclientad.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # vi:ts=4 sw=4 sts=4 3 | 4 | # Copyright (C) 2017 Red Hat, Inc. 5 | # 6 | # This program is free software; you can redistribute it and/or 7 | # modify it under the terms of the GNU Lesser General Public 8 | # License as published by the Free Software Foundation; either 9 | # version 2.1 of the licence, or (at your option) any later version. 10 | # 11 | # This program is distributed in the hope that it will be useful, 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 14 | # Lesser General Public License for more details. 15 | # 16 | # You should have received a copy of the GNU Lesser General Public 17 | # License along with this program; if not, see . 18 | # 19 | # Authors: Alberto Ruiz 20 | # Oliver Gutiérrez 21 | 22 | import logging 23 | 24 | import dbus 25 | import dbus.service 26 | import dbus.mainloop.glib 27 | 28 | from gi.repository import GObject 29 | 30 | from fleetcommanderclient.configloader import ConfigLoader 31 | from fleetcommanderclient import adapters 32 | 33 | DBUS_BUS_NAME = "org.freedesktop.FleetCommanderClientAD" 34 | DBUS_OBJECT_PATH = "/org/freedesktop/FleetCommanderClientAD" 35 | DBUS_INTERFACE_NAME = "org.freedesktop.FleetCommanderClientAD" 36 | 37 | 38 | class FleetCommanderClientADDbusService(dbus.service.Object): 39 | 40 | """ 41 | Fleet commander client d-bus service class 42 | """ 43 | 44 | _loop = None 45 | 46 | def __init__(self, configfile="/etc/xdg/fleet-commander-client.conf"): 47 | """ 48 | Class initialization 49 | """ 50 | # Load configuration options 51 | self.config = ConfigLoader(configfile) 52 | 53 | # Set logging level 54 | self.log_level = self.config.get_value("log_level") 55 | loglevel = getattr(logging, self.log_level.upper()) 56 | logging.basicConfig(level=loglevel) 57 | 58 | # Configuration adapters 59 | self.adapters = {} 60 | 61 | self.register_adapter( 62 | adapters.DconfAdapter, 63 | self.config.get_value("dconf_profile_path"), 64 | self.config.get_value("dconf_db_path"), 65 | ) 66 | 67 | self.register_adapter( 68 | adapters.GOAAdapter, self.config.get_value("goa_run_path") 69 | ) 70 | 71 | self.register_adapter(adapters.NetworkManagerAdapter) 72 | 73 | self.register_adapter( 74 | adapters.ChromiumAdapter, self.config.get_value("chromium_policies_path") 75 | ) 76 | 77 | self.register_adapter( 78 | adapters.ChromeAdapter, self.config.get_value("chrome_policies_path") 79 | ) 80 | 81 | self.register_adapter( 82 | adapters.FirefoxAdapter, self.config.get_value("firefox_prefs_path") 83 | ) 84 | 85 | # Parent initialization 86 | super().__init__() 87 | 88 | def run(self, sessionbus=False): 89 | dbus.mainloop.glib.DBusGMainLoop(set_as_default=True) 90 | if not sessionbus: 91 | bus = dbus.SystemBus() 92 | else: 93 | bus = dbus.SessionBus() 94 | bus_name = dbus.service.BusName(DBUS_BUS_NAME, bus) 95 | dbus.service.Object.__init__(self, bus_name, DBUS_OBJECT_PATH) 96 | self._loop = GObject.MainLoop() 97 | 98 | # Enter main loop 99 | self._loop.run() 100 | 101 | def quit(self): 102 | self._loop.quit() 103 | 104 | def register_adapter(self, adapterclass, *args, **kwargs): 105 | self.adapters[adapterclass.NAMESPACE] = adapterclass(*args, **kwargs) 106 | 107 | def get_peer_uid(self, sender): 108 | proxy = dbus.SystemBus().get_object("org.freedesktop.DBus", "/") 109 | interface = dbus.Interface(proxy, dbus_interface="org.freedesktop.DBus") 110 | return interface.GetConnectionUnixUser(sender) 111 | 112 | @dbus.service.method( 113 | DBUS_INTERFACE_NAME, 114 | in_signature="", 115 | out_signature="", 116 | message_keyword="dbusmessage", 117 | ) 118 | def ProcessFiles(self, dbusmessage): 119 | 120 | logging.debug("FC Client: Applying user configuration") 121 | 122 | # Get peer UID for security 123 | uid = self.get_peer_uid(dbusmessage.get_sender()) 124 | 125 | logging.debug("FC Client: Got peer UID: %s", uid) 126 | 127 | # Cycle through configuration adapters and deploy existing data 128 | for namespace, adapter in self.adapters.items(): 129 | logging.debug( 130 | "FC Client: Deploying configuration for namespace %s", namespace 131 | ) 132 | adapter.deploy(uid) 133 | self.quit() 134 | 135 | @dbus.service.method(DBUS_INTERFACE_NAME, in_signature="", out_signature="") 136 | def Quit(self): 137 | self.quit() 138 | 139 | 140 | if __name__ == "__main__": 141 | svc = FleetCommanderClientADDbusService() 142 | svc.run() 143 | -------------------------------------------------------------------------------- /tests/08_configadapter_firefoxbookmarks.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | # vi:ts=2 sw=2 sts=2 4 | 5 | # Copyright (C) 2017 Red Hat, Inc. 6 | # 7 | # This program is free software; you can redistribute it and/or 8 | # modify it under the terms of the GNU Lesser General Public 9 | # License as published by the Free Software Foundation; either 10 | # version 2.1 of the licence, or (at your option) any later version. 11 | # 12 | # This program is distributed in the hope that it will be useful, 13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 15 | # Lesser General Public License for more details. 16 | # 17 | # You should have received a copy of the GNU Lesser General Public 18 | # License along with this program; if not, see . 19 | # 20 | # Authors: Alberto Ruiz 21 | # Oliver Gutiérrez 22 | 23 | import os 24 | import sys 25 | import tempfile 26 | import shutil 27 | import json 28 | import unittest 29 | import logging 30 | import stat 31 | 32 | sys.path.append(os.path.join(os.environ["TOPSRCDIR"], "src")) 33 | 34 | import fleetcommanderclient.configadapters.firefoxbookmarks 35 | from fleetcommanderclient.configadapters.firefoxbookmarks import ( 36 | FirefoxBookmarksConfigAdapter, 37 | ) 38 | 39 | # Set logging to debug 40 | logging.basicConfig(level=logging.DEBUG) 41 | 42 | PROFILE_FILE_CONTENTS = r"""{ 43 | "org.mozilla.firefox.Bookmarks": [ 44 | { 45 | "key": "blah", 46 | "value": { 47 | "Title": "Test bookmark", 48 | "URL": "https://example.com", 49 | "Favicon": "https://example.com/favicon.ico", 50 | "Placement": "toolbar", 51 | "Folder": "FolderName" 52 | } 53 | } 54 | ] 55 | }""" 56 | 57 | POLICIES_FILE_CONTENTS = { 58 | "policies": { 59 | "Bookmarks": [ 60 | { 61 | "Title": "Test bookmark", 62 | "URL": "https://example.com", 63 | "Favicon": "https://example.com/favicon.ico", 64 | "Placement": "toolbar", 65 | "Folder": "FolderName", 66 | } 67 | ] 68 | } 69 | } 70 | 71 | 72 | def universal_function(*args, **kwargs): 73 | pass 74 | 75 | 76 | # Monkey patch chown function in os module for firefox config adapter 77 | fleetcommanderclient.configadapters.firefoxbookmarks.os.chown = universal_function 78 | 79 | 80 | class TestFirefoxBookmarksConfigAdapter(unittest.TestCase): 81 | TEST_UID = os.getuid() 82 | 83 | TEST_DATA = json.loads(PROFILE_FILE_CONTENTS)["org.mozilla.firefox.Bookmarks"] 84 | 85 | def setUp(self): 86 | self.test_directory = tempfile.mkdtemp(prefix="fc-client-firefoxbookmarks-test") 87 | policies_path_template = os.path.join(self.test_directory, "{}/firefox") 88 | self.policies_path = policies_path_template.format(self.TEST_UID) 89 | self.policies_file_path = os.path.join( 90 | self.policies_path, FirefoxBookmarksConfigAdapter.POLICIES_FILENAME 91 | ) 92 | self.ca = FirefoxBookmarksConfigAdapter(policies_path_template) 93 | 94 | def tearDown(self): 95 | # Remove test directory 96 | shutil.rmtree(self.test_directory) 97 | 98 | def test_00_bootstrap(self): 99 | logging.debug("Paths do not exist yet") 100 | self.assertFalse(os.path.exists(self.policies_file_path)) 101 | self.assertFalse(os.path.isdir(self.policies_path)) 102 | 103 | logging.debug("Run bootstrap with no directory created should continue") 104 | self.ca.bootstrap(self.TEST_UID) 105 | 106 | logging.debug("Run bootstrap with existing directories") 107 | os.makedirs(self.policies_path) 108 | with open(self.policies_file_path, "w") as fd: 109 | fd.write("POLICIES") 110 | fd.close() 111 | self.assertTrue(os.path.isdir(self.policies_path)) 112 | self.assertTrue(os.path.exists(self.policies_file_path)) 113 | self.ca.bootstrap(self.TEST_UID) 114 | 115 | logging.debug("Check file has been removed") 116 | self.assertFalse(os.path.exists(self.policies_file_path)) 117 | self.assertTrue(os.path.isdir(self.policies_path)) 118 | 119 | def test_01_update(self): 120 | logging.debug("Run bootstrap") 121 | self.ca.bootstrap(self.TEST_UID) 122 | 123 | logging.debug("Run update") 124 | self.ca.update(self.TEST_UID, self.TEST_DATA) 125 | 126 | logging.debug("Check file has been written") 127 | self.assertTrue(os.path.exists(self.policies_file_path)) 128 | 129 | logging.debug("Check file contents") 130 | # Change file mod because test user haven't root privilege 131 | os.chmod(self.policies_file_path, stat.S_IRUSR) 132 | with open(self.policies_file_path, "r") as fd: 133 | data = json.loads(fd.read()) 134 | fd.close() 135 | self.assertEqual( 136 | json.dumps(POLICIES_FILE_CONTENTS, sort_keys=True), 137 | json.dumps(data, sort_keys=True), 138 | ) 139 | 140 | 141 | if __name__ == "__main__": 142 | unittest.main() 143 | -------------------------------------------------------------------------------- /tests/05_configadapter_dconf.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | # vi:ts=2 sw=2 sts=2 4 | 5 | # Copyright (C) 2017 Red Hat, Inc. 6 | # 7 | # This program is free software; you can redistribute it and/or 8 | # modify it under the terms of the GNU Lesser General Public 9 | # License as published by the Free Software Foundation; either 10 | # version 2.1 of the licence, or (at your option) any later version. 11 | # 12 | # This program is distributed in the hope that it will be useful, 13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 15 | # Lesser General Public License for more details. 16 | # 17 | # You should have received a copy of the GNU Lesser General Public 18 | # License along with this program; if not, see . 19 | # 20 | # Authors: Alberto Ruiz 21 | # Oliver Gutiérrez 22 | 23 | import os 24 | import sys 25 | import tempfile 26 | import shutil 27 | import unittest 28 | 29 | from gi.repository import GLib 30 | 31 | sys.path.append(os.path.join(os.environ["TOPSRCDIR"], "src")) 32 | 33 | 34 | from fleetcommanderclient.configadapters.dconf import DconfConfigAdapter 35 | 36 | 37 | class TestDconfConfigAdapter(unittest.TestCase): 38 | 39 | TEST_UID = 55555 40 | 41 | TEST_DATA = [ 42 | { 43 | "signature": "s", 44 | "value": "'#CCCCCC'", 45 | "key": "/org/yorba/shotwell/preferences/ui/background-color", 46 | "schema": "org.yorba.shotwell.preferences.ui", 47 | }, 48 | { 49 | "key": "/org/gnome/software/popular-overrides", 50 | "value": "['riot.desktop','matrix.desktop']", 51 | "signature": "as", 52 | }, 53 | ] 54 | 55 | def setUp(self): 56 | self.test_directory = tempfile.mkdtemp(prefix="fc-client-dconf-test") 57 | 58 | self.profiledir = os.path.join(self.test_directory, "profile") 59 | self.dbdir = os.path.join(self.test_directory, "db") 60 | self.kfdir = os.path.join( 61 | self.dbdir, "%s%s.d" % (DconfConfigAdapter.FC_DB_FILE, str(self.TEST_UID)) 62 | ) 63 | self.profilepath = os.path.join(self.profiledir, str(self.TEST_UID)) 64 | self.kfpath = os.path.join(self.kfdir, DconfConfigAdapter.FC_PROFILE_FILE) 65 | self.dbpath = os.path.join( 66 | self.dbdir, "%s%s" % (DconfConfigAdapter.FC_DB_FILE, str(self.TEST_UID)) 67 | ) 68 | 69 | self.ca = DconfConfigAdapter( 70 | os.path.join(self.test_directory, "profile"), 71 | os.path.join(self.test_directory, "db"), 72 | ) 73 | 74 | def tearDown(self): 75 | # Remove test directory 76 | shutil.rmtree(self.test_directory) 77 | 78 | def test_00_bootstrap(self): 79 | # Run bootstrap with no directory created should continue and warn 80 | self.ca.bootstrap(self.TEST_UID) 81 | # Run bootstrap with existing directories 82 | os.makedirs(self.profiledir) 83 | os.makedirs(self.dbdir) 84 | os.makedirs(self.kfdir) 85 | with open(self.profilepath, "w") as fd: 86 | fd.write("PROFILE_FILE") 87 | fd.close() 88 | with open(self.kfpath, "w") as fd: 89 | fd.write("KEY_FILE") 90 | fd.close() 91 | with open(self.dbpath, "w") as fd: 92 | fd.write("DB_FILE") 93 | fd.close() 94 | self.assertTrue(os.path.isdir(self.profiledir)) 95 | self.assertTrue(os.path.isdir(self.kfdir)) 96 | self.assertTrue(os.path.isdir(self.dbdir)) 97 | self.assertTrue(os.path.exists(self.profilepath)) 98 | self.assertTrue(os.path.exists(self.kfpath)) 99 | self.assertTrue(os.path.exists(self.dbpath)) 100 | self.ca.bootstrap(self.TEST_UID) 101 | # Check files and directories had been removed 102 | self.assertFalse(os.path.exists(self.profilepath)) 103 | self.assertFalse(os.path.exists(self.kfpath)) 104 | self.assertFalse(os.path.exists(self.dbpath)) 105 | self.assertTrue(os.path.isdir(self.profiledir)) 106 | self.assertFalse(os.path.isdir(self.kfdir)) 107 | self.assertTrue(os.path.isdir(self.dbdir)) 108 | 109 | def test_01_update(self): 110 | self.ca.bootstrap(self.TEST_UID) 111 | self.ca.update(self.TEST_UID, self.TEST_DATA) 112 | # Check keyfile has been written 113 | self.assertTrue(os.path.exists(self.kfpath)) 114 | # Read keyfile 115 | keyfile = GLib.KeyFile.new() 116 | keyfile.load_from_file(self.kfpath, GLib.KeyFileFlags.NONE) 117 | 118 | # Check all sections 119 | for item in self.TEST_DATA: 120 | # Check all keys and values 121 | keysplit = item["key"][1:].split("/") 122 | keypath = "/".join(keysplit[:-1]) 123 | keyname = keysplit[-1] 124 | value = item["value"] 125 | value_keyfile = keyfile.get_string(keypath, keyname) 126 | self.assertEqual(value, value_keyfile) 127 | 128 | # Check db file has been compiled 129 | self.assertTrue(os.path.exists(self.dbpath)) 130 | with open(self.dbpath, "r") as fd: 131 | data = fd.read() 132 | fd.close() 133 | self.assertEqual(data, "COMPILED\n") 134 | 135 | 136 | if __name__ == "__main__": 137 | unittest.main() 138 | -------------------------------------------------------------------------------- /src/fleetcommanderclient/configadapters/networkmanager.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # vi:ts=4 sw=4 sts=4 3 | 4 | # Copyright (C) 2017 Red Hat, Inc. 5 | # 6 | # This program is free software; you can redistribute it and/or 7 | # modify it under the terms of the GNU Lesser General Public 8 | # License as published by the Free Software Foundation; either 9 | # version 2.1 of the licence, or (at your option) any later version. 10 | # 11 | # This program is distributed in the hope that it will be useful, 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 14 | # Lesser General Public License for more details. 15 | # 16 | # You should have received a copy of the GNU Lesser General Public 17 | # License along with this program; if not, see . 18 | # 19 | # Authors: Alberto Ruiz 20 | # Oliver Gutiérrez 21 | 22 | 23 | import logging 24 | import uuid 25 | import pwd 26 | 27 | import gi 28 | 29 | gi.require_version("NM", "1.0") 30 | 31 | from gi.repository import Gio 32 | from gi.repository import GLib 33 | from gi.repository import NM 34 | 35 | from fleetcommanderclient.configadapters.base import BaseConfigAdapter 36 | 37 | 38 | class NetworkManagerDbusHelper: 39 | """ 40 | Network manager dbus helper 41 | """ 42 | 43 | BUS_NAME = "org.freedesktop.NetworkManager" 44 | DBUS_OBJECT_PATH = "/org/freedesktop/NetworkManager/Settings" 45 | DBUS_INTERFACE_NAME = "org.freedesktop.NetworkManager.Settings" 46 | 47 | def __init__(self): 48 | self.bus = Gio.bus_get_sync(Gio.BusType.SYSTEM, None) 49 | self.client = NM.Client.new(None) 50 | 51 | def get_user_name(self, uid): 52 | return pwd.getpwuid(uid).pw_name 53 | 54 | def get_connection_path_by_uuid(self, conn_uuid): 55 | """ 56 | Returns connection path as an string 57 | """ 58 | conn = self.client.get_connection_by_uuid(conn_uuid) 59 | if conn: 60 | return conn.get_path() 61 | return None 62 | 63 | def add_connection(self, connection_data): 64 | return self.bus.call_sync( 65 | self.BUS_NAME, 66 | self.DBUS_OBJECT_PATH, 67 | self.DBUS_INTERFACE_NAME, 68 | "AddConnection", 69 | GLib.Variant.new_tuple(connection_data), 70 | GLib.VariantType("(o)"), 71 | Gio.DBusCallFlags.NONE, 72 | -1, 73 | None, 74 | ) 75 | 76 | def update_connection(self, connection_path, connection_data): 77 | return self.bus.call_sync( 78 | self.BUS_NAME, 79 | connection_path, 80 | self.DBUS_INTERFACE_NAME + ".Connection", 81 | "Update", 82 | GLib.Variant.new_tuple(connection_data), 83 | GLib.VariantType("()"), 84 | Gio.DBusCallFlags.NONE, 85 | -1, 86 | None, 87 | ) 88 | 89 | 90 | class NetworkManagerConfigAdapter(BaseConfigAdapter): 91 | """ 92 | Configuration adapter for Network Manager 93 | """ 94 | 95 | NAMESPACE = "org.freedesktop.NetworkManager" 96 | 97 | def __init__(self): 98 | self.nmhelper = NetworkManagerDbusHelper() 99 | 100 | def bootstrap(self, uid): 101 | pass 102 | 103 | def add_connection_metadata(self, serialized_data, uname, conn_uuid): 104 | sc = NM.SimpleConnection.new_from_dbus( 105 | GLib.Variant.parse(None, serialized_data, None, None) 106 | ) 107 | setu = sc.get_setting(NM.SettingUser) 108 | if not setu: 109 | sc.add_setting(NM.SettingUser()) 110 | setu = sc.get_setting(NM.SettingUser) 111 | 112 | setc = sc.get_setting(NM.SettingConnection) 113 | 114 | hashed_uuid = str(uuid.uuid5(uuid.UUID(conn_uuid), uname)) 115 | 116 | setu.set_data("org.fleet-commander.connection", "true") 117 | setu.set_data("org.fleet-commander.connection.uuid", conn_uuid) 118 | setc.set_property("uuid", hashed_uuid) 119 | setc.add_permission("user", uname, None) 120 | 121 | return (sc.to_dbus(NM.ConnectionSerializationFlags.NO_SECRETS), hashed_uuid) 122 | 123 | def update(self, uid, data): 124 | uname = self.nmhelper.get_user_name(uid) 125 | for connection in data: 126 | conn_uuid = connection["uuid"] 127 | connection_data, hashed_uuid = self.add_connection_metadata( 128 | connection["data"], uname, conn_uuid 129 | ) 130 | 131 | logging.debug( 132 | "Checking connection %s + %s -> %s", conn_uuid, uname, hashed_uuid 133 | ) 134 | # Check if connection already exist 135 | path = self.nmhelper.get_connection_path_by_uuid(hashed_uuid) 136 | 137 | if path is not None: 138 | try: 139 | self.nmhelper.update_connection(path, connection_data) 140 | except Exception as e: 141 | logging.error("Error updating connection %s: %s", conn_uuid, e) 142 | else: 143 | # Connection does not exist. Add it 144 | try: 145 | self.nmhelper.add_connection(connection_data) 146 | except Exception as e: 147 | # Error adding connection 148 | logging.error("Error adding connection %s: %s", conn_uuid, e) 149 | -------------------------------------------------------------------------------- /tests/07_configadapter_firefox.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | # vi:ts=2 sw=2 sts=2 4 | 5 | # Copyright (C) 2017 Red Hat, Inc. 6 | # 7 | # This program is free software; you can redistribute it and/or 8 | # modify it under the terms of the GNU Lesser General Public 9 | # License as published by the Free Software Foundation; either 10 | # version 2.1 of the licence, or (at your option) any later version. 11 | # 12 | # This program is distributed in the hope that it will be useful, 13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 15 | # Lesser General Public License for more details. 16 | # 17 | # You should have received a copy of the GNU Lesser General Public 18 | # License along with this program; if not, see . 19 | # 20 | # Authors: Alberto Ruiz 21 | # Oliver Gutiérrez 22 | 23 | import os 24 | import sys 25 | import tempfile 26 | import shutil 27 | import json 28 | import unittest 29 | import logging 30 | import stat 31 | 32 | sys.path.append(os.path.join(os.environ["TOPSRCDIR"], "src")) 33 | 34 | import fleetcommanderclient.configadapters.firefox 35 | from fleetcommanderclient.configadapters.firefox import FirefoxConfigAdapter 36 | 37 | # Set logging to debug 38 | logging.basicConfig(level=logging.DEBUG) 39 | 40 | PROFILE_FILE_CONTENTS = r"""{"org.mozilla.firefox": [{"value": 0, "key": "accessibility.typeaheadfind.flashBar"}, {"value": false, "key": "beacon.enabled"}, {"value": "{\"placements\":{\"widget-overflow-fixed-list\":[],\"PersonalToolbar\":[\"personal-bookmarks\"],\"nav-bar\":[\"back-button\",\"forward-button\",\"stop-reload-button\",\"home-button\",\"customizableui-special-spring1\",\"urlbar-container\",\"customizableui-special-spring2\",\"downloads-button\",\"library-button\",\"sidebar-button\"],\"TabsToolbar\":[\"tabbrowser-tabs\",\"new-tab-button\",\"alltabs-button\"],\"toolbar-menubar\":[\"menubar-items\"]},\"seen\":[\"developer-button\"],\"dirtyAreaCache\":[\"PersonalToolbar\",\"nav-bar\",\"TabsToolbar\",\"toolbar-menubar\"],\"currentVersion\":12,\"newElementCount\":2}", "key": "browser.uiCustomization.state"}], "com.google.chrome.Policies": [], "org.chromium.Policies": [], "org.gnome.gsettings": [], "org.libreoffice.registry": [], "org.freedesktop.NetworkManager": []}""" 41 | 42 | PREFS_FILE_CONTENTS = r"""pref("accessibility.typeaheadfind.flashBar", 0); 43 | pref("beacon.enabled", false); 44 | pref("browser.uiCustomization.state", "{\"placements\":{\"widget-overflow-fixed-list\":[],\"PersonalToolbar\":[\"personal-bookmarks\"],\"nav-bar\":[\"back-button\",\"forward-button\",\"stop-reload-button\",\"home-button\",\"customizableui-special-spring1\",\"urlbar-container\",\"customizableui-special-spring2\",\"downloads-button\",\"library-button\",\"sidebar-button\"],\"TabsToolbar\":[\"tabbrowser-tabs\",\"new-tab-button\",\"alltabs-button\"],\"toolbar-menubar\":[\"menubar-items\"]},\"seen\":[\"developer-button\"],\"dirtyAreaCache\":[\"PersonalToolbar\",\"nav-bar\",\"TabsToolbar\",\"toolbar-menubar\"],\"currentVersion\":12,\"newElementCount\":2}");""" 45 | 46 | 47 | def universal_function(*args, **kwargs): 48 | pass 49 | 50 | 51 | # Monkey patch chown function in os module for firefox config adapter 52 | fleetcommanderclient.configadapters.firefox.os.chown = universal_function 53 | 54 | 55 | class TestFirefoxConfigAdapter(unittest.TestCase): 56 | 57 | TEST_UID = os.getuid() 58 | 59 | TEST_DATA = json.loads(PROFILE_FILE_CONTENTS)["org.mozilla.firefox"] 60 | 61 | def setUp(self): 62 | self.test_directory = tempfile.mkdtemp(prefix="fc-client-firefox-test") 63 | self.policies_path = os.path.join(self.test_directory, "managed") 64 | self.policies_file_path = os.path.join( 65 | self.policies_path, FirefoxConfigAdapter.PREFS_FILENAME % self.TEST_UID 66 | ) 67 | self.ca = FirefoxConfigAdapter(self.policies_path) 68 | 69 | def tearDown(self): 70 | # Remove test directory 71 | shutil.rmtree(self.test_directory) 72 | 73 | def test_00_bootstrap(self): 74 | # Run bootstrap with no directory created should continue 75 | self.ca.bootstrap(self.TEST_UID) 76 | # Run bootstrap with existing directories 77 | os.makedirs(self.policies_path) 78 | with open(self.policies_file_path, "w") as fd: 79 | fd.write("PREFS") 80 | fd.close() 81 | self.assertTrue(os.path.isdir(self.policies_path)) 82 | self.assertTrue(os.path.exists(self.policies_file_path)) 83 | self.ca.bootstrap(self.TEST_UID) 84 | # Check file has been removed 85 | self.assertFalse(os.path.exists(self.policies_file_path)) 86 | self.assertTrue(os.path.isdir(self.policies_path)) 87 | 88 | def test_01_update(self): 89 | self.ca.bootstrap(self.TEST_UID) 90 | self.ca.update(self.TEST_UID, self.TEST_DATA) 91 | # Check file has been written 92 | self.assertTrue(os.path.exists(self.policies_file_path)) 93 | # Change file mod because test user haven't root privilege 94 | os.chmod(self.policies_file_path, stat.S_IRUSR) 95 | # Read file 96 | with open(self.policies_file_path, "r") as fd: 97 | data = fd.read() 98 | fd.close() 99 | # Check file contents are ok 100 | self.assertEqual(PREFS_FILE_CONTENTS, data) 101 | 102 | 103 | if __name__ == "__main__": 104 | unittest.main() 105 | -------------------------------------------------------------------------------- /tests/_fcclientad_tests.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | # vi:ts=4 sw=4 sts=4 4 | 5 | # Copyright (C) 2017 Red Hat, Inc. 6 | # 7 | # This program is free software; you can redistribute it and/or 8 | # modify it under the terms of the GNU Lesser General Public 9 | # License as published by the Free Software Foundation; either 10 | # version 2.1 of the licence, or (at your option) any later version. 11 | # 12 | # This program is distributed in the hope that it will be useful, 13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 15 | # Lesser General Public License for more details. 16 | # 17 | # You should have received a copy of the GNU Lesser General Public 18 | # License along with this program; if not, see . 19 | # 20 | # Authors: Alberto Ruiz 21 | # Oliver Gutiérrez 22 | 23 | # Python imports 24 | import os 25 | import sys 26 | import tempfile 27 | import subprocess 28 | import time 29 | import unittest 30 | 31 | import dbus 32 | 33 | PYTHONPATH = os.path.join(os.environ["TOPSRCDIR"], "src") 34 | sys.path.append(PYTHONPATH) 35 | 36 | # Fleet commander imports 37 | from fleetcommanderclient import fcclientad 38 | 39 | 40 | class FleetCommanderClientADDbusClient: 41 | """ 42 | Fleet commander client dbus client 43 | """ 44 | 45 | DEFAULT_BUS = dbus.SessionBus 46 | CONNECTION_TIMEOUT = 2 47 | 48 | def __init__(self, bus=None): 49 | """ 50 | Class initialization 51 | """ 52 | if bus is None: 53 | bus = self.DEFAULT_BUS() 54 | self.bus = bus 55 | 56 | t = time.time() 57 | while time.time() - t < self.CONNECTION_TIMEOUT: 58 | try: 59 | self.obj = self.bus.get_object( 60 | fcclientad.DBUS_BUS_NAME, fcclientad.DBUS_OBJECT_PATH 61 | ) 62 | self.iface = dbus.Interface( 63 | self.obj, dbus_interface=fcclientad.DBUS_INTERFACE_NAME 64 | ) 65 | return 66 | except Exception: 67 | pass 68 | raise Exception("Timed out connecting to fleet commander client dbus service") 69 | 70 | def process_files(self): 71 | return self.iface.ProcessFiles() 72 | 73 | 74 | class TestDbusClient(FleetCommanderClientADDbusClient): 75 | DEFAULT_BUS = dbus.SessionBus 76 | 77 | def test_service_alive(self): 78 | return self.iface.TestServiceAlive() 79 | 80 | 81 | # Mock dbus client 82 | fcclientad.FleetCommanderClientADDbusClient = TestDbusClient 83 | 84 | 85 | class TestDbusService(unittest.TestCase): 86 | 87 | maxDiff = None 88 | MAX_DBUS_CHECKS = 1 89 | 90 | CACHE_FILEPATHS = [ 91 | "org.gnome.online-accounts/fleet-commander-accounts.conf", 92 | ] 93 | 94 | def setUp(self): 95 | self.test_directory = tempfile.mkdtemp() 96 | 97 | # Execute dbus service 98 | self.service = subprocess.Popen( 99 | [ 100 | os.path.join( 101 | os.environ["TOPSRCDIR"], "tests/test_fcclientad_service.py" 102 | ), 103 | self.test_directory, 104 | ], 105 | stdout=subprocess.PIPE, 106 | stderr=subprocess.PIPE, 107 | ) 108 | 109 | checks = 0 110 | while True: 111 | try: 112 | c = self.get_client() 113 | c.test_service_alive() 114 | break 115 | except Exception as e: 116 | checks += 1 117 | if checks < self.MAX_DBUS_CHECKS: 118 | time.sleep(0.1) 119 | else: 120 | self.service.kill() 121 | self.print_dbus_service_output() 122 | raise Exception( 123 | "DBUS service taking too much time to start: %s" % e 124 | ) 125 | 126 | def tearDown(self): 127 | # Kill service 128 | self.service.kill() 129 | self.print_dbus_service_output() 130 | # shutil.rmtree(self.test_directory) 131 | 132 | def print_dbus_service_output(self): 133 | print("------- BEGIN DBUS SERVICE STDOUT -------") 134 | print(self.service.stdout.read()) 135 | print("-------- END DBUS SERVICE STDOUT --------") 136 | print("------- BEGIN DBUS SERVICE STDERR -------") 137 | print(self.service.stderr.read()) 138 | print("-------- END DBUS SERVICE STDERR --------") 139 | 140 | def get_client(self): 141 | return TestDbusClient() 142 | 143 | def test_00_process_files(self): 144 | c = self.get_client() 145 | 146 | # Create fake compiled files where dbus service expect them 147 | for fpath in self.CACHE_FILEPATHS: 148 | fname = os.path.join(self.test_directory, "cache", fpath) 149 | fdir = os.path.dirname(fname) 150 | if not os.path.isdir(fdir): 151 | os.makedirs(fdir) 152 | with open(fname, "w") as fd: 153 | fd.write("{}") 154 | fd.close() 155 | 156 | c.process_files() 157 | 158 | # Check GOA accounts file has been deployed 159 | self.assertTrue( 160 | os.path.isfile( 161 | os.path.join( 162 | self.test_directory, 163 | "run/goa-1.0/55555/fleet-commander-accounts.conf", 164 | ) 165 | ) 166 | ) 167 | 168 | 169 | if __name__ == "__main__": 170 | unittest.main() 171 | -------------------------------------------------------------------------------- /tests/12_adapter_firefox.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | # vi:ts=2 sw=2 sts=2 4 | 5 | # Copyright (C) 2019 Red Hat, Inc. 6 | # 7 | # This program is free software; you can redistribute it and/or 8 | # modify it under the terms of the GNU Lesser General Public 9 | # License as published by the Free Software Foundation; either 10 | # version 2.1 of the licence, or (at your option) any later version. 11 | # 12 | # This program is distributed in the hope that it will be useful, 13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 15 | # Lesser General Public License for more details. 16 | # 17 | # You should have received a copy of the GNU Lesser General Public 18 | # License along with this program; if not, see . 19 | # 20 | # Authors: Alberto Ruiz 21 | # Oliver Gutiérrez 22 | 23 | import os 24 | import sys 25 | import logging 26 | import tempfile 27 | import shutil 28 | import json 29 | import unittest 30 | 31 | sys.path.append(os.path.join(os.environ["TOPSRCDIR"], "src")) 32 | 33 | import fleetcommanderclient.adapters.firefox 34 | from fleetcommanderclient.adapters.firefox import FirefoxAdapter 35 | 36 | 37 | def universal_function(*args, **kwargs): 38 | pass 39 | 40 | 41 | # Monkey patch chown function in os module 42 | fleetcommanderclient.adapters.firefox.os.chown = universal_function 43 | 44 | 45 | # Set log level to debug 46 | logging.basicConfig(level=logging.DEBUG) 47 | 48 | PROFILE_FILE_CONTENTS = r"""{"org.mozilla.firefox": [{"value": 0, "key": "accessibility.typeaheadfind.flashBar"}, {"value": false, "key": "beacon.enabled"}, {"value": "{\"placements\":{\"widget-overflow-fixed-list\":[],\"PersonalToolbar\":[\"personal-bookmarks\"],\"nav-bar\":[\"back-button\",\"forward-button\",\"stop-reload-button\",\"home-button\",\"customizableui-special-spring1\",\"urlbar-container\",\"customizableui-special-spring2\",\"downloads-button\",\"library-button\",\"sidebar-button\"],\"TabsToolbar\":[\"tabbrowser-tabs\",\"new-tab-button\",\"alltabs-button\"],\"toolbar-menubar\":[\"menubar-items\"]},\"seen\":[\"developer-button\"],\"dirtyAreaCache\":[\"PersonalToolbar\",\"nav-bar\",\"TabsToolbar\",\"toolbar-menubar\"],\"currentVersion\":12,\"newElementCount\":2}", "key": "browser.uiCustomization.state"}], "com.google.chrome.Policies": [], "org.chromium.Policies": [], "org.gnome.gsettings": [], "org.libreoffice.registry": [], "org.freedesktop.NetworkManager": []}""" 49 | 50 | PREFS_FILE_CONTENTS = r"""pref("accessibility.typeaheadfind.flashBar", 0); 51 | pref("beacon.enabled", false); 52 | pref("browser.uiCustomization.state", "{\"placements\":{\"widget-overflow-fixed-list\":[],\"PersonalToolbar\":[\"personal-bookmarks\"],\"nav-bar\":[\"back-button\",\"forward-button\",\"stop-reload-button\",\"home-button\",\"customizableui-special-spring1\",\"urlbar-container\",\"customizableui-special-spring2\",\"downloads-button\",\"library-button\",\"sidebar-button\"],\"TabsToolbar\":[\"tabbrowser-tabs\",\"new-tab-button\",\"alltabs-button\"],\"toolbar-menubar\":[\"menubar-items\"]},\"seen\":[\"developer-button\"],\"dirtyAreaCache\":[\"PersonalToolbar\",\"nav-bar\",\"TabsToolbar\",\"toolbar-menubar\"],\"currentVersion\":12,\"newElementCount\":2}");""" 53 | 54 | 55 | class TestFirefoxAdapter(unittest.TestCase): 56 | 57 | TEST_UID = 55555 58 | 59 | TEST_DATA = json.loads(PROFILE_FILE_CONTENTS)["org.mozilla.firefox"] 60 | 61 | def setUp(self): 62 | self.test_directory = tempfile.mkdtemp(prefix="fc-client-firefox-test") 63 | self.prefs_path = self.test_directory 64 | self.cache_path = os.path.join(self.test_directory, "cache") 65 | self.prefs_file_path = os.path.join( 66 | self.prefs_path, FirefoxAdapter.PREFS_FILENAME.format(self.TEST_UID) 67 | ) 68 | self.ca = FirefoxAdapter(self.prefs_path) 69 | self.ca._TEST_CACHE_PATH = self.cache_path 70 | 71 | def tearDown(self): 72 | # Remove test directory 73 | shutil.rmtree(self.test_directory) 74 | 75 | def test_00_generate_config(self): 76 | # Generate configuration 77 | self.ca.generate_config(self.TEST_DATA) 78 | # Check configuration file exists 79 | filepath = os.path.join(self.cache_path, self.ca.NAMESPACE, "fleet-commander") 80 | logging.debug("Checking %s exists", filepath) 81 | self.assertTrue(os.path.exists(filepath)) 82 | # Check configuration file contents 83 | with open(filepath, "r") as fd: 84 | data = fd.read() 85 | fd.close() 86 | self.assertEqual(data, PREFS_FILE_CONTENTS) 87 | 88 | def test_01_deploy(self): 89 | # Generate config files in cache 90 | self.ca.generate_config(self.TEST_DATA) 91 | # Execute deployment 92 | self.ca.deploy(self.TEST_UID) 93 | # Check file has been copied to policies path 94 | deployed_file_path = os.path.join( 95 | self.prefs_path, FirefoxAdapter.PREFS_FILENAME.format(self.TEST_UID) 96 | ) 97 | self.assertTrue(os.path.isfile(deployed_file_path)) 98 | # Check both files content is the same 99 | with open(deployed_file_path, "r") as fd: 100 | data1 = fd.read() 101 | fd.close() 102 | cached_file_path = os.path.join( 103 | self.cache_path, self.ca.NAMESPACE, "fleet-commander" 104 | ) 105 | with open(cached_file_path, "r") as fd: 106 | data2 = fd.read() 107 | fd.close() 108 | self.assertEqual(data1, data2) 109 | 110 | 111 | if __name__ == "__main__": 112 | unittest.main() 113 | -------------------------------------------------------------------------------- /src/fleetcommanderclient/fcclient.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # vi:ts=4 sw=4 sts=4 3 | 4 | # Copyright (C) 2017 Red Hat, Inc. 5 | # 6 | # This program is free software; you can redistribute it and/or 7 | # modify it under the terms of the GNU Lesser General Public 8 | # License as published by the Free Software Foundation; either 9 | # version 2.1 of the licence, or (at your option) any later version. 10 | # 11 | # This program is distributed in the hope that it will be useful, 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 14 | # Lesser General Public License for more details. 15 | # 16 | # You should have received a copy of the GNU Lesser General Public 17 | # License along with this program; if not, see . 18 | # 19 | # Authors: Alberto Ruiz 20 | # Oliver Gutiérrez 21 | 22 | import logging 23 | 24 | import dbus 25 | import dbus.service 26 | import dbus.mainloop.glib 27 | 28 | from gi.repository import GObject 29 | 30 | from fleetcommanderclient.configloader import ConfigLoader 31 | from fleetcommanderclient import configadapters 32 | from fleetcommanderclient.settingscompiler import SettingsCompiler 33 | 34 | DBUS_BUS_NAME = "org.freedesktop.FleetCommanderClient" 35 | DBUS_OBJECT_PATH = "/org/freedesktop/FleetCommanderClient" 36 | DBUS_INTERFACE_NAME = "org.freedesktop.FleetCommanderClient" 37 | 38 | 39 | class FleetCommanderClientDbusService(dbus.service.Object): 40 | 41 | """ 42 | Fleet commander client d-bus service class 43 | """ 44 | 45 | _loop = None 46 | 47 | def __init__(self, configfile="/etc/xdg/fleet-commander-client.conf"): 48 | """ 49 | Class initialization 50 | """ 51 | # Load configuration options 52 | self.config = ConfigLoader(configfile) 53 | 54 | # Set logging level 55 | self.log_level = self.config.get_value("log_level") 56 | loglevel = getattr(logging, self.log_level.upper()) 57 | logging.basicConfig(level=loglevel) 58 | 59 | # Configuration adapters (old) 60 | self.config_adapters = {} 61 | 62 | self.register_config_adapter( 63 | configadapters.DconfConfigAdapter, 64 | self.config.get_value("dconf_profile_path"), 65 | self.config.get_value("dconf_db_path"), 66 | ) 67 | 68 | self.register_config_adapter( 69 | configadapters.GOAConfigAdapter, self.config.get_value("goa_run_path") 70 | ) 71 | 72 | self.register_config_adapter(configadapters.NetworkManagerConfigAdapter) 73 | 74 | self.register_config_adapter( 75 | configadapters.ChromiumConfigAdapter, 76 | self.config.get_value("chromium_policies_path"), 77 | ) 78 | 79 | self.register_config_adapter( 80 | configadapters.ChromeConfigAdapter, 81 | self.config.get_value("chrome_policies_path"), 82 | ) 83 | 84 | self.register_config_adapter( 85 | configadapters.FirefoxConfigAdapter, 86 | self.config.get_value("firefox_prefs_path"), 87 | ) 88 | 89 | self.register_config_adapter( 90 | configadapters.FirefoxBookmarksConfigAdapter, 91 | self.config.get_value("firefox_policies_path"), 92 | ) 93 | 94 | # Parent initialization 95 | super().__init__() 96 | 97 | def run(self, sessionbus=False): 98 | dbus.mainloop.glib.DBusGMainLoop(set_as_default=True) 99 | if not sessionbus: 100 | bus = dbus.SystemBus() 101 | else: 102 | bus = dbus.SessionBus() 103 | bus_name = dbus.service.BusName(DBUS_BUS_NAME, bus) 104 | dbus.service.Object.__init__(self, bus_name, DBUS_OBJECT_PATH) 105 | self._loop = GObject.MainLoop() 106 | 107 | # Enter main loop 108 | self._loop.run() 109 | 110 | def quit(self): 111 | self._loop.quit() 112 | 113 | def register_config_adapter(self, adapterclass, *args, **kwargs): 114 | self.config_adapters[adapterclass.NAMESPACE] = adapterclass(*args, **kwargs) 115 | 116 | @dbus.service.method(DBUS_INTERFACE_NAME, in_signature="usq", out_signature="") 117 | def ProcessSSSDFiles(self, uid, directory, policy): 118 | """ 119 | Types: 120 | uid: Unsigned 32 bit integer (Real local user ID) 121 | directory: String (Path where the files has been deployed by SSSD) 122 | policy: Unsigned 16 bit integer (as specified in FreeIPA) 123 | """ 124 | 125 | logging.debug( 126 | "FC Client: SSSD Data received - %s - %s - %s", uid, directory, policy 127 | ) 128 | # Compile settings 129 | sc = SettingsCompiler(directory) 130 | logging.debug("FC Client: Compiling settings") 131 | compiled_settings = sc.compile_settings() 132 | # Send data to configuration adapters 133 | logging.debug("FC Client: Applying settings") 134 | for namespace in compiled_settings: 135 | logging.debug("FC Client: Checking adapters for namespace %s", namespace) 136 | if namespace in self.config_adapters: 137 | logging.debug( 138 | "FC Client: Applying settings for namespace %s", namespace 139 | ) 140 | self.config_adapters[namespace].bootstrap(uid) 141 | data = compiled_settings[namespace] 142 | self.config_adapters[namespace].update(uid, data) 143 | self.quit() 144 | 145 | @dbus.service.method(DBUS_INTERFACE_NAME, in_signature="", out_signature="") 146 | def Quit(self): 147 | self.quit() 148 | 149 | 150 | if __name__ == "__main__": 151 | svc = FleetCommanderClientDbusService() 152 | svc.run() 153 | -------------------------------------------------------------------------------- /src/fleetcommanderclient/configadapters/dconf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # vi:ts=4 sw=4 sts=4 3 | 4 | # Copyright (C) 2017 Red Hat, Inc. 5 | # 6 | # This program is free software; you can redistribute it and/or 7 | # modify it under the terms of the GNU Lesser General Public 8 | # License as published by the Free Software Foundation; either 9 | # version 2.1 of the licence, or (at your option) any later version. 10 | # 11 | # This program is distributed in the hope that it will be useful, 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 14 | # Lesser General Public License for more details. 15 | # 16 | # You should have received a copy of the GNU Lesser General Public 17 | # License along with this program; if not, see . 18 | # 19 | # Authors: Alberto Ruiz 20 | # Oliver Gutiérrez 21 | 22 | import os 23 | import shutil 24 | import logging 25 | import subprocess 26 | 27 | from gi.repository import GLib 28 | 29 | from fleetcommanderclient.configadapters.base import BaseConfigAdapter 30 | 31 | 32 | class DconfConfigAdapter(BaseConfigAdapter): 33 | """ 34 | Configuration adapter for Dconf 35 | """ 36 | 37 | NAMESPACE = "org.gnome.gsettings" 38 | FC_PROFILE_FILE = "fleet-commander-dconf.conf" 39 | FC_DB_FILE = "fleet-commander-dconf-" 40 | 41 | def __init__(self, dconf_profile_path, dconf_db_path): 42 | self.dconf_profile_path = dconf_profile_path 43 | self.dconf_db_path = dconf_db_path 44 | 45 | def get_paths_for_uid(self, uid): 46 | profile_path = os.path.join(self.dconf_profile_path, str(uid)) 47 | keyfile_dir = os.path.join( 48 | self.dconf_db_path, "%s%s.d" % (self.FC_DB_FILE, str(uid)) 49 | ) 50 | db_path = os.path.join(self.dconf_db_path, "%s%s" % (self.FC_DB_FILE, str(uid))) 51 | return (profile_path, keyfile_dir, db_path) 52 | 53 | def remove_path(self, path, throw=False): 54 | logging.debug('Removing path: "%s"', path) 55 | try: 56 | if os.path.exists(path): 57 | if os.path.isdir(path): 58 | shutil.rmtree(path) 59 | else: 60 | os.remove(path) 61 | except Exception as e: 62 | if throw: 63 | logging.error('Error removing path "%s": %s', path, e) 64 | raise e 65 | logging.warning('Error removing path "%s": %s', path, e) 66 | 67 | def bootstrap(self, uid): 68 | # Remove old data 69 | profile_path, keyfile_dir, db_path = self.get_paths_for_uid(uid) 70 | self.remove_path(profile_path) 71 | self.remove_path(db_path) 72 | self.remove_path(keyfile_dir) 73 | 74 | def update(self, uid, data): 75 | profile_path, keyfile_dir, db_path = self.get_paths_for_uid(uid) 76 | 77 | # Prepare data for saving it in keyfile 78 | logging.debug("Preparing dconf data for saving to keyfile") 79 | keyfile = GLib.KeyFile.new() 80 | for item in data: 81 | if "key" in item and "value" in item: 82 | keysplit = item["key"][1:].split("/") 83 | keypath = "/".join(keysplit[:-1]) 84 | keyname = keysplit[-1] 85 | keyfile.set_string(keypath, keyname, item["value"]) 86 | 87 | # Create keyfile path 88 | logging.debug('Creating keyfile path for dconf: "%s"', profile_path) 89 | try: 90 | os.makedirs(keyfile_dir) 91 | except Exception as e: 92 | logging.error('Error creating keyfile path "%s": %s', profile_path, e) 93 | return 94 | 95 | # Save config file 96 | keyfile_path = os.path.join(keyfile_dir, self.FC_PROFILE_FILE) 97 | logging.debug('Saving dconf keyfile to "%s"', keyfile_path) 98 | try: 99 | keyfile.save_to_file(keyfile_path) 100 | except Exception as e: 101 | logging.error('Error saving dconf keyfile at "%s": %s', keyfile_path, e) 102 | return 103 | 104 | # Compile dconf database 105 | try: 106 | self._compile_dconf_db(uid) 107 | except Exception as e: 108 | logging.error('Error compiling dconf data to "%s": %s', db_path, e) 109 | return 110 | 111 | # Create runtime path 112 | logging.debug('Creating profile path for dconf: "%s"', profile_path) 113 | try: 114 | os.makedirs(self.dconf_profile_path) 115 | except Exception: 116 | pass 117 | try: 118 | profile_data = "user-db:user\n\nsystem-db:%s%s" % (self.FC_DB_FILE, uid) 119 | with open(profile_path, "w") as fd: 120 | fd.write(profile_data) 121 | fd.close() 122 | except Exception as e: 123 | logging.error('Error saving dconf profile at "%s": %s', profile_path, e) 124 | return 125 | 126 | logging.info("Processed dconf configuration for UID %s", uid) 127 | 128 | def _compile_dconf_db(self, uid): 129 | """ 130 | Compiles dconf database 131 | """ 132 | keyfile_dir, db_path = self.get_paths_for_uid(uid)[1:] 133 | 134 | # Execute dbus service 135 | with subprocess.Popen( 136 | [ 137 | "dconf", 138 | "compile", 139 | db_path, 140 | keyfile_dir, 141 | ], 142 | stdout=subprocess.PIPE, 143 | stderr=subprocess.PIPE, 144 | ) as cmd: 145 | cmd.wait() 146 | out = cmd.stdout.read() 147 | err = cmd.stderr.read() 148 | cmd.stdout.close() 149 | cmd.stderr.close() 150 | if cmd.returncode != 0: 151 | raise Exception("%s\n%s" % (out, err)) 152 | -------------------------------------------------------------------------------- /tests/14_adapter_dconf.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | # vi:ts=2 sw=2 sts=2 4 | 5 | # Copyright (C) 2019 Red Hat, Inc. 6 | # 7 | # This program is free software; you can redistribute it and/or 8 | # modify it under the terms of the GNU Lesser General Public 9 | # License as published by the Free Software Foundation; either 10 | # version 2.1 of the licence, or (at your option) any later version. 11 | # 12 | # This program is distributed in the hope that it will be useful, 13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 15 | # Lesser General Public License for more details. 16 | # 17 | # You should have received a copy of the GNU Lesser General Public 18 | # License along with this program; if not, see . 19 | # 20 | # Authors: Alberto Ruiz 21 | # Oliver Gutiérrez 22 | 23 | import os 24 | import sys 25 | import logging 26 | import tempfile 27 | import shutil 28 | import stat 29 | import unittest 30 | 31 | from gi.repository import GLib 32 | 33 | sys.path.append(os.path.join(os.environ["TOPSRCDIR"], "src")) 34 | 35 | import fleetcommanderclient.adapters.dconf 36 | from fleetcommanderclient.adapters.dconf import DconfAdapter 37 | 38 | 39 | def universal_function(*args, **kwargs): 40 | pass 41 | 42 | 43 | # Monkey patch chown function in os module for chromium config adapter 44 | fleetcommanderclient.adapters.dconf.os.chown = universal_function 45 | 46 | 47 | # Set log level to debug 48 | logging.basicConfig(level=logging.DEBUG) 49 | 50 | 51 | class TestDconfAdapter(unittest.TestCase): 52 | 53 | TEST_UID = 55555 54 | 55 | TEST_DATA = [ 56 | { 57 | "signature": "s", 58 | "value": "'#CCCCCC'", 59 | "key": "/org/yorba/shotwell/preferences/ui/background-color", 60 | "schema": "org.yorba.shotwell.preferences.ui", 61 | }, 62 | { 63 | "key": "/org/gnome/software/popular-overrides", 64 | "value": "['riot.desktop','matrix.desktop']", 65 | "signature": "as", 66 | }, 67 | ] 68 | 69 | DCONF_USER_FILE_CONTENTS = "user-db:user\n\nsystem-db:{}" 70 | 71 | def setUp(self): 72 | self.test_directory = tempfile.mkdtemp(prefix="fc-client-dconf-test") 73 | self.cache_path = os.path.join(self.test_directory, "cache") 74 | self.ca = DconfAdapter(self.test_directory, self.test_directory) 75 | self.ca._TEST_CACHE_PATH = self.cache_path 76 | 77 | def tearDown(self): 78 | # Change permissions of directories to allow removal 79 | runtime_dir = os.path.join(self.test_directory, str(self.TEST_UID)) 80 | if os.path.exists(runtime_dir): 81 | os.chmod(runtime_dir, stat.S_IREAD | stat.S_IWRITE | stat.S_IEXEC) 82 | # Remove test directory 83 | shutil.rmtree(self.test_directory) 84 | 85 | def test_00_generate_config(self): 86 | # Generate configuration 87 | self.ca.generate_config(self.TEST_DATA) 88 | # Check keyfiles dir exist 89 | keyfiles_dir = os.path.join(self.cache_path, self.ca.NAMESPACE, "keyfiles") 90 | self.assertTrue(os.path.isdir(keyfiles_dir)) 91 | 92 | # Check keyfile exists 93 | keyfile_path = os.path.join(keyfiles_dir, self.ca.PROFILE_FILE) 94 | self.assertTrue(os.path.isfile(keyfile_path)) 95 | 96 | # Read keyfile 97 | keyfile = GLib.KeyFile.new() 98 | keyfile.load_from_file(keyfile_path, GLib.KeyFileFlags.NONE) 99 | 100 | # Check all sections 101 | for item in self.TEST_DATA: 102 | # Check all keys and values 103 | keysplit = item["key"][1:].split("/") 104 | keypath = "/".join(keysplit[:-1]) 105 | keyname = keysplit[-1] 106 | value = item["value"] 107 | value_keyfile = keyfile.get_string(keypath, keyname) 108 | self.assertEqual(value, value_keyfile) 109 | 110 | # Check db file exists 111 | dbfile_path = os.path.join(self.cache_path, self.ca.NAMESPACE, self.ca.DB_FILE) 112 | self.assertTrue(os.path.isfile(dbfile_path)) 113 | 114 | # Check db file contents 115 | with open(dbfile_path, "r") as fd: 116 | data = fd.read() 117 | fd.close() 118 | self.assertEqual(data, "COMPILED\n") 119 | 120 | def test_01_deploy(self): 121 | # Generate config files in cache 122 | self.ca.generate_config(self.TEST_DATA) 123 | # Execute deployment 124 | self.ca.deploy(self.TEST_UID) 125 | 126 | # Check db file has been copied to db path 127 | deployed_file_name = "{}-{}".format(self.ca.DB_FILE, self.TEST_UID) 128 | deployed_file_path = os.path.join(self.test_directory, deployed_file_name) 129 | self.assertTrue(os.path.isfile(deployed_file_path)) 130 | 131 | # Check both files content is the same 132 | with open(deployed_file_path, "r") as fd: 133 | data1 = fd.read() 134 | fd.close() 135 | cached_file_path = os.path.join( 136 | self.cache_path, self.ca.NAMESPACE, self.ca.DB_FILE 137 | ) 138 | with open(cached_file_path, "r") as fd: 139 | data2 = fd.read() 140 | fd.close() 141 | self.assertEqual(data1, data2) 142 | 143 | # Check user dconf file has been created 144 | dconf_user_file_path = os.path.join( 145 | self.test_directory, "{}".format(self.TEST_UID) 146 | ) 147 | self.assertTrue(os.path.isfile(deployed_file_path)) 148 | 149 | # Check both files content is the same 150 | with open(dconf_user_file_path, "r") as fd: 151 | data = fd.read() 152 | fd.close() 153 | self.assertEqual(data, self.DCONF_USER_FILE_CONTENTS.format(deployed_file_name)) 154 | 155 | 156 | if __name__ == "__main__": 157 | unittest.main() 158 | -------------------------------------------------------------------------------- /tests/_fcclient_tests.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | # vi:ts=4 sw=4 sts=4 4 | 5 | # Copyright (C) 2017 Red Hat, Inc. 6 | # 7 | # This program is free software; you can redistribute it and/or 8 | # modify it under the terms of the GNU Lesser General Public 9 | # License as published by the Free Software Foundation; either 10 | # version 2.1 of the licence, or (at your option) any later version. 11 | # 12 | # This program is distributed in the hope that it will be useful, 13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 15 | # Lesser General Public License for more details. 16 | # 17 | # You should have received a copy of the GNU Lesser General Public 18 | # License along with this program; if not, see . 19 | # 20 | # Authors: Alberto Ruiz 21 | # Oliver Gutiérrez 22 | 23 | # Python imports 24 | import os 25 | import sys 26 | import shutil 27 | import tempfile 28 | import subprocess 29 | import time 30 | import unittest 31 | 32 | import dbus 33 | 34 | PYTHONPATH = os.path.join(os.environ["TOPSRCDIR"], "src") 35 | sys.path.append(PYTHONPATH) 36 | 37 | # Fleet commander imports 38 | from fleetcommanderclient import fcclient 39 | 40 | 41 | class FleetCommanderClientDbusClient: 42 | """ 43 | Fleet commander client dbus client 44 | """ 45 | 46 | DEFAULT_BUS = dbus.SessionBus 47 | CONNECTION_TIMEOUT = 2 48 | 49 | def __init__(self, bus=None): 50 | """ 51 | Class initialization 52 | """ 53 | if bus is None: 54 | bus = self.DEFAULT_BUS() 55 | self.bus = bus 56 | 57 | t = time.time() 58 | while time.time() - t < self.CONNECTION_TIMEOUT: 59 | try: 60 | self.obj = self.bus.get_object( 61 | fcclient.DBUS_BUS_NAME, fcclient.DBUS_OBJECT_PATH 62 | ) 63 | self.iface = dbus.Interface( 64 | self.obj, dbus_interface=fcclient.DBUS_INTERFACE_NAME 65 | ) 66 | return 67 | except Exception: 68 | pass 69 | raise Exception("Timed out connecting to fleet commander client dbus service") 70 | 71 | def process_sssd_files(self, uid, directory, policy): 72 | """ 73 | Types: 74 | uid: Unsigned 32 bit integer (Real local user ID) 75 | directory: String (Path where the files has been deployed by SSSD) 76 | policy: Unsigned 16 bit integer (as specified in FreeIPA) 77 | """ 78 | return self.iface.ProcessSSSDFiles(uid, directory, policy) 79 | 80 | 81 | class TestDbusClient(FleetCommanderClientDbusClient): 82 | DEFAULT_BUS = dbus.SessionBus 83 | 84 | def test_service_alive(self): 85 | return self.iface.TestServiceAlive() 86 | 87 | 88 | # Mock dbus client 89 | fcclient.FleetCommanderClientDbusClient = TestDbusClient 90 | 91 | 92 | class TestDbusService(unittest.TestCase): 93 | 94 | maxDiff = None 95 | MAX_DBUS_CHECKS = 1 96 | TEST_UID = 55555 97 | TEST_POLICY = 23 98 | 99 | def setUp(self): 100 | self.test_directory = tempfile.mkdtemp() 101 | 102 | # Execute dbus service 103 | self.service = subprocess.Popen( 104 | [ 105 | os.path.join(os.environ["TOPSRCDIR"], "tests/test_fcclient_service.py"), 106 | self.test_directory, 107 | ], 108 | stdout=subprocess.PIPE, 109 | stderr=subprocess.PIPE, 110 | ) 111 | 112 | checks = 0 113 | while True: 114 | try: 115 | c = self.get_client() 116 | c.test_service_alive() 117 | break 118 | except Exception as e: 119 | checks += 1 120 | if checks < self.MAX_DBUS_CHECKS: 121 | time.sleep(0.1) 122 | else: 123 | self.service.kill() 124 | self.print_dbus_service_output() 125 | raise Exception( 126 | "DBUS service taking too much time to start: %s" % e 127 | ) 128 | 129 | def tearDown(self): 130 | # Kill service 131 | self.service.kill() 132 | self.print_dbus_service_output() 133 | shutil.rmtree(self.test_directory) 134 | 135 | def print_dbus_service_output(self): 136 | print("------- BEGIN DBUS SERVICE STDOUT -------") 137 | print(self.service.stdout.read()) 138 | print("-------- END DBUS SERVICE STDOUT --------") 139 | print("------- BEGIN DBUS SERVICE STDERR -------") 140 | print(self.service.stderr.read()) 141 | print("-------- END DBUS SERVICE STDERR --------") 142 | 143 | def get_client(self): 144 | return TestDbusClient() 145 | 146 | def test_00_process_sssd_files(self): 147 | c = self.get_client() 148 | directory = os.path.join( 149 | os.environ["TOPSRCDIR"], "tests/data/sampleprofiledata/" 150 | ) 151 | c.process_sssd_files(self.TEST_UID, directory, self.TEST_POLICY) 152 | 153 | # Check dconf settings db has been deployed 154 | self.assertTrue( 155 | os.path.isfile( 156 | os.path.join( 157 | self.test_directory, "etc/dconf/db/fleet-commander-dconf-55555" 158 | ) 159 | ) 160 | ) 161 | # Check dconf user database config has been deployed 162 | self.assertTrue( 163 | os.path.isfile(os.path.join(self.test_directory, "run/dconf/user/55555")) 164 | ) 165 | # Check GOA accounts file has been deployed 166 | self.assertTrue( 167 | os.path.isfile( 168 | os.path.join( 169 | self.test_directory, 170 | "run/goa-1.0/55555/fleet-commander-accounts.conf", 171 | ) 172 | ) 173 | ) 174 | 175 | self.assertEqual(True, True) 176 | 177 | 178 | if __name__ == "__main__": 179 | unittest.main() 180 | -------------------------------------------------------------------------------- /tests/ldapmock.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # vi:ts=2 sw=2 sts=2 3 | 4 | # Copyright (C) 2019 Red Hat, Inc. 5 | # 6 | # This program is free software; you can redistribute it and/or 7 | # modify it under the terms of the GNU Lesser General Public 8 | # License as published by the Free Software Foundation; either 9 | # version 2.1 of the licence, or (at your option) any later version. 10 | # 11 | # This program is distributed in the hope that it will be useful, 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 14 | # Lesser General Public License for more details. 15 | # 16 | # You should have received a copy of the GNU Lesser General Public 17 | # License along with this program; if not, see . 18 | # 19 | # Authors: Alberto Ruiz 20 | # Oliver Gutiérrez 21 | 22 | # Python imports 23 | import logging 24 | 25 | 26 | DOMAIN_DATA = {} 27 | 28 | 29 | class SASLMock: 30 | @staticmethod 31 | def sasl(cb_value_dict, mech): 32 | # We asume auth is OK as long as mechanism is GSSAPI 33 | # We ignore callbacks 34 | if mech == "GSSAPI": 35 | return "AUTH OK" 36 | raise Exception("SASLMock: Auth mechanism is not GSSAPI (Kerberos)") 37 | 38 | 39 | class LDAPConnectionMock: 40 | 41 | protocol_version = 3 42 | 43 | def __init__(self, server_address): 44 | logging.debug("LDAPMock initializing connection: %s", server_address) 45 | self.server_address = server_address 46 | self.options = {} 47 | self._domain_data = DOMAIN_DATA 48 | 49 | def _ldif_to_ldap_data(self, ldif): 50 | data = {} 51 | for elem in ldif: 52 | data[elem[0]] = (elem[1],) 53 | return data 54 | 55 | def set_option(self, key, value): 56 | self.options[key] = value 57 | 58 | def sasl_interactive_bind_s(self, who, sasl_auth): 59 | # We assume auth is ok if who is '' and sasl_auth was created using sasl 60 | if who == "" and sasl_auth == "AUTH OK": 61 | return 62 | raise Exception( 63 | "SASLMock: Incorrect parameters for SASL binding: %s, %s" % (who, sasl_auth) 64 | ) 65 | 66 | def search_s( 67 | self, 68 | base, 69 | scope, 70 | filterstr="(objectClass=*)", 71 | attrlist=None, 72 | attrsonly=0, 73 | timeout=-1, 74 | ): 75 | logging.debug("LDAPMock search_s: %s - %s", base, filterstr) 76 | if base == "DC=FC,DC=AD": 77 | groupfilter = "(&(objectclass=group)(CN=" 78 | sidfilter = "(&(|(objectclass=computer)(objectclass=user)(objectclass=group))(objectSid=" 79 | if filterstr == "(objectClass=*)" and attrlist == ["objectSid"]: 80 | return (("cn", self._domain_data["domain"]),) 81 | if sidfilter in filterstr: 82 | filtersid = filterstr[len(sidfilter) : -2] 83 | for objclass in ["users", "groups", "hosts"]: 84 | for elem in self._domain_data[objclass].values(): 85 | # Use unpacked object sid to avoid use of ndr_unpack 86 | if filtersid == elem["unpackedObjectSid"]: 87 | return [ 88 | (elem["cn"], elem), 89 | ] 90 | elif groupfilter in filterstr: 91 | groupname = filterstr[len(groupfilter) : -2] 92 | if groupname in self._domain_data["groups"].keys(): 93 | return (("cn", self._domain_data["groups"][groupname]),) 94 | elif base == "CN=Users,DC=FC,DC=AD": 95 | userfilter = "(&(objectclass=user)(CN=" 96 | if userfilter in filterstr: 97 | username = filterstr[len(userfilter) : -2] 98 | if username in self._domain_data["users"].keys(): 99 | return (("cn", self._domain_data["users"][username]),) 100 | elif base == "CN=Computers,DC=FC,DC=AD": 101 | hostfilter = "(&(objectclass=computer)(CN=" 102 | if hostfilter in filterstr: 103 | hostname = filterstr[len(hostfilter) : -2] 104 | if hostname in self._domain_data["hosts"].keys(): 105 | return (("cn", self._domain_data["hosts"][hostname]),) 106 | elif base == "CN=Policies,CN=System,DC=FC,DC=AD": 107 | if filterstr == "(objectclass=groupPolicyContainer)": 108 | profile_list = [] 109 | for cn in self._domain_data["profiles"].keys(): 110 | profile_list.append((cn, self._domain_data["profiles"][cn])) 111 | return profile_list 112 | if "(displayName=" in filterstr: 113 | displayname = filterstr[len("(displayName=") : -1] 114 | # Trying to get a profile by its display name 115 | for elem in self._domain_data["profiles"].values(): 116 | if elem["displayName"][0].decode() == displayname: 117 | return [(elem["cn"], elem)] 118 | else: 119 | cn = "CN=%s,CN=Policies,CN=System,DC=FC,DC=AD" % filterstr[4:-1] 120 | if cn in self._domain_data["profiles"].keys(): 121 | return [(cn, self._domain_data["profiles"][cn])] 122 | return [] 123 | 124 | def add_s(self, dn, ldif): 125 | self._domain_data["profiles"][dn] = self._ldif_to_ldap_data(ldif) 126 | 127 | def modify_s(self, dn, ldif): 128 | profile = self._domain_data["profiles"][dn] 129 | for dif in ldif: 130 | value = (dif[2],) 131 | if dif[1] in ["displayName", "description"]: 132 | value = (dif[2],) 133 | profile[dif[1]] = value 134 | 135 | def delete_s(self, dn): 136 | logging.debug("LDAPMock: delete_s %s", dn) 137 | if dn in self._domain_data["profiles"].keys(): 138 | del self._domain_data["profiles"][dn] 139 | 140 | 141 | # Mock sasl module 142 | sasl = SASLMock 143 | 144 | 145 | # Constants 146 | OPT_REFERRALS = 1 147 | SCOPE_SUBTREE = 2 148 | SCOPE_BASE = 3 149 | MOD_REPLACE = 4 150 | 151 | 152 | # Functions 153 | def initialize(server_address): 154 | return LDAPConnectionMock(server_address) 155 | -------------------------------------------------------------------------------- /src/fleetcommanderclient/adapters/nm.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # vi:ts=4 sw=4 sts=4 3 | 4 | # Copyright (C) 2017 Red Hat, Inc. 5 | # 6 | # This program is free software; you can redistribute it and/or 7 | # modify it under the terms of the GNU Lesser General Public 8 | # License as published by the Free Software Foundation; either 9 | # version 2.1 of the licence, or (at your option) any later version. 10 | # 11 | # This program is distributed in the hope that it will be useful, 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 14 | # Lesser General Public License for more details. 15 | # 16 | # You should have received a copy of the GNU Lesser General Public 17 | # License along with this program; if not, see . 18 | # 19 | # Authors: Alberto Ruiz 20 | # Oliver Gutiérrez 21 | 22 | import os 23 | import logging 24 | import uuid 25 | import pwd 26 | import json 27 | 28 | import gi 29 | 30 | gi.require_version("NM", "1.0") 31 | 32 | from gi.repository import Gio 33 | from gi.repository import GLib 34 | from gi.repository import NM 35 | 36 | from fleetcommanderclient.adapters.base import BaseAdapter 37 | 38 | 39 | class NetworkManagerDbusHelper: 40 | """ 41 | Network manager dbus helper 42 | """ 43 | 44 | BUS_NAME = "org.freedesktop.NetworkManager" 45 | DBUS_OBJECT_PATH = "/org/freedesktop/NetworkManager/Settings" 46 | DBUS_INTERFACE_NAME = "org.freedesktop.NetworkManager.Settings" 47 | 48 | def __init__(self): 49 | self.bus = Gio.bus_get_sync(Gio.BusType.SYSTEM, None) 50 | self.client = NM.Client.new(None) 51 | 52 | def get_user_name(self, uid): 53 | return pwd.getpwuid(uid).pw_name 54 | 55 | def get_connection_path_by_uuid(self, conn_uuid): 56 | """ 57 | Returns connection path as an string 58 | """ 59 | conn = self.client.get_connection_by_uuid(conn_uuid) 60 | if conn: 61 | return conn.get_path() 62 | return None 63 | 64 | def add_connection(self, connection_data): 65 | return self.bus.call_sync( 66 | self.BUS_NAME, 67 | self.DBUS_OBJECT_PATH, 68 | self.DBUS_INTERFACE_NAME, 69 | "AddConnection", 70 | GLib.Variant.new_tuple(connection_data), 71 | GLib.VariantType("(o)"), 72 | Gio.DBusCallFlags.NONE, 73 | -1, 74 | None, 75 | ) 76 | 77 | def update_connection(self, connection_path, connection_data): 78 | return self.bus.call_sync( 79 | self.BUS_NAME, 80 | connection_path, 81 | self.DBUS_INTERFACE_NAME + ".Connection", 82 | "Update", 83 | GLib.Variant.new_tuple(connection_data), 84 | GLib.VariantType("()"), 85 | Gio.DBusCallFlags.NONE, 86 | -1, 87 | None, 88 | ) 89 | 90 | 91 | class NetworkManagerAdapter(BaseAdapter): 92 | """ 93 | Configuration adapter for Network Manager 94 | """ 95 | 96 | NAMESPACE = "org.freedesktop.NetworkManager" 97 | 98 | def _add_connection_metadata(self, serialized_data, uname, conn_uuid): 99 | sc = NM.SimpleConnection.new_from_dbus( 100 | GLib.Variant.parse(None, serialized_data, None, None) 101 | ) 102 | setu = sc.get_setting(NM.SettingUser) 103 | if not setu: 104 | sc.add_setting(NM.SettingUser()) 105 | setu = sc.get_setting(NM.SettingUser) 106 | 107 | setc = sc.get_setting(NM.SettingConnection) 108 | 109 | hashed_uuid = str(uuid.uuid5(uuid.UUID(conn_uuid), uname)) 110 | 111 | setu.set_data("org.fleet-commander.connection", "true") 112 | setu.set_data("org.fleet-commander.connection.uuid", conn_uuid) 113 | setc.set_property("uuid", hashed_uuid) 114 | setc.add_permission("user", uname, None) 115 | 116 | return (sc.to_dbus(NM.ConnectionSerializationFlags.NO_SECRETS), hashed_uuid) 117 | 118 | def process_config_data(self, config_data, cache_path): 119 | """ 120 | Process configuration data and save cache files to be deployed 121 | """ 122 | # Write data as JSON 123 | path = os.path.join(cache_path, "fleet-commander") 124 | logging.debug("Writing NM data to %s", path) 125 | with open(path, "w") as fd: 126 | fd.write(json.dumps(config_data)) 127 | fd.close() 128 | 129 | def deploy_files(self, cache_path, uid): 130 | """ 131 | Create connections using NM dbus service 132 | This method will be called by privileged process 133 | """ 134 | path = os.path.join(cache_path, "fleet-commander") 135 | 136 | if os.path.isfile(path): 137 | logging.debug("Deploying connections from file %s", path) 138 | nmhelper = NetworkManagerDbusHelper() 139 | uname = nmhelper.get_user_name(uid) 140 | with open(path, "r") as fd: 141 | data = json.loads(fd.read()) 142 | fd.close() 143 | 144 | for connection in data: 145 | conn_uuid = connection["uuid"] 146 | connection_data, hashed_uuid = self._add_connection_metadata( 147 | connection["data"], uname, conn_uuid 148 | ) 149 | logging.debug( 150 | "Checking connection %s + %s -> %s", conn_uuid, uname, hashed_uuid 151 | ) 152 | 153 | # Check if connection already exist 154 | path = nmhelper.get_connection_path_by_uuid(hashed_uuid) 155 | 156 | if path is not None: 157 | try: 158 | nmhelper.update_connection(path, connection_data) 159 | except Exception as e: 160 | logging.error("Error updating connection %s: %s", conn_uuid, e) 161 | else: 162 | # Connection does not exist. Add it 163 | try: 164 | nmhelper.add_connection(connection_data) 165 | except Exception as e: 166 | # Error adding connection 167 | logging.error("Error adding connection %s: %s", conn_uuid, e) 168 | else: 169 | logging.debug("Connections file %s is not present. Ignoring.", path) 170 | -------------------------------------------------------------------------------- /fleet-commander-client.spec: -------------------------------------------------------------------------------- 1 | # This package depends on automagic byte compilation 2 | # https://fedoraproject.org/wiki/Changes/No_more_automagic_Python_bytecompilation_phase_2 3 | %global _python_bytecompile_extra 1 4 | 5 | Name: fleet-commander-client 6 | Version: 0.16.0 7 | Release: 1%{?dist} 8 | Summary: Fleet Commander Client 9 | 10 | BuildArch: noarch 11 | 12 | License: LGPLv3+ and LGPLv2+ and MIT and BSD 13 | URL: https://raw.githubusercontent.com/fleet-commander/fc-client/master/fleet-commander-client.spec 14 | Source0: https://github.com/fleet-commander/fc-client/releases/download/%{version}/%{name}-%{version}.tar.xz 15 | 16 | 17 | BuildRequires: dconf 18 | 19 | %if 0%{?fedora} >= 30 20 | BuildRequires: python3-devel 21 | BuildRequires: python3-mock 22 | BuildRequires: python3-gobject 23 | BuildRequires: python3-dbus 24 | BuildRequires: python3-dbusmock 25 | BuildRequires: python3-samba 26 | %endif 27 | 28 | %if 0%{?with_check} 29 | BuildRequires: git 30 | BuildRequires: dbus 31 | BuildRequires: python3-mock 32 | BuildRequires: python3-dns 33 | BuildRequires: python3-ldap 34 | BuildRequires: python3-dbusmock 35 | BuildRequires: python3-ipalib 36 | BuildRequires: python3-samba 37 | BuildRequires: NetworkManager-libnm 38 | BuildRequires: json-glib 39 | BuildRequires: NetworkManager 40 | %endif 41 | 42 | Requires: NetworkManager 43 | Requires: NetworkManager-libnm 44 | Requires: systemd 45 | Requires: dconf 46 | Requires(preun): systemd 47 | 48 | 49 | %if 0%{?fedora} >= 30 50 | Requires: python3 51 | BuildRequires: python3-gobject 52 | Requires: python3-samba 53 | Requires: python3-dns 54 | Requires: python3-ldap 55 | %endif 56 | 57 | %description 58 | Profile data retriever for Fleet Commander client hosts. Fleet Commander is an 59 | application that allows you to manage the desktop configuration of a large 60 | network of users and workstations/laptops. 61 | 62 | %prep 63 | %setup -q 64 | 65 | %build 66 | %configure --with-systemdsystemunitdir=%{_unitdir} 67 | %configure --with-systemduserunitdir=%{_userunitdir} 68 | %make_build 69 | 70 | %install 71 | %make_install 72 | 73 | %preun 74 | %systemd_preun fleet-commander-client.service 75 | %systemd_preun fleet-commander-clientad.service 76 | %systemd_user_preun fleet-commander-adretriever.service 77 | 78 | %post 79 | %systemd_post fleet-commander-client.service 80 | %systemd_post fleet-commander-clientad.service 81 | %systemd_user_post fleet-commander-adretriever.service 82 | 83 | %postun 84 | %systemd_postun_with_restart fleet-commander-client.service 85 | %systemd_postun_with_restart fleet-commander-clientad.service 86 | %systemd_user_postun_with_restart fleet-commander-adretriever.service 87 | 88 | %files 89 | %license 90 | %dir %{_datadir}/%{name} 91 | %dir %{_datadir}/%{name}/python 92 | %dir %{_datadir}/%{name}/python/fleetcommanderclient 93 | %attr(644, -, -) %{_datadir}/%{name}/python/fleetcommanderclient/*.py 94 | %dir %{_datadir}/%{name}/python/fleetcommanderclient/configadapters 95 | %attr(644, -, -) %{_datadir}/%{name}/python/fleetcommanderclient/configadapters/*.py 96 | %dir %{_datadir}/%{name}/python/fleetcommanderclient/adapters 97 | %attr(644, -, -) %{_datadir}/%{name}/python/fleetcommanderclient/adapters/*.py 98 | %config(noreplace) %{_sysconfdir}/xdg/%{name}.conf 99 | %config(noreplace) %{_sysconfdir}/dbus-1/system.d/org.freedesktop.FleetCommanderClient.conf 100 | %config(noreplace) %{_sysconfdir}/dbus-1/system.d/org.freedesktop.FleetCommanderClientAD.conf 101 | %{_unitdir}/fleet-commander-client.service 102 | %{_unitdir}/fleet-commander-clientad.service 103 | %{_userunitdir}/fleet-commander-adretriever.service 104 | %{_datadir}/dbus-1/system-services/org.freedesktop.FleetCommanderClient.service 105 | %{_datadir}/dbus-1/system-services/org.freedesktop.FleetCommanderClientAD.service 106 | 107 | %if 0%{?rhel} && 0%{?rhel} < 8 108 | %attr(644, -, -) %{_datadir}/%{name}/python/fleetcommanderclient/*.py[co] 109 | %attr(644, -, -) %{_datadir}/%{name}/python/fleetcommanderclient/configadapters/*.py[co] 110 | %attr(644, -, -) %{_datadir}/%{name}/python/fleetcommanderclient/adapters/*.py[co] 111 | %endif 112 | 113 | 114 | %changelog 115 | * Wed May 19 2021 Oliver Gutierrez - 0.16.0-1 116 | - Deprecation of python2 117 | 118 | * Tue Jan 28 2020 Fedora Release Engineering - 0.15.0-3 119 | - Rebuilt for https://fedoraproject.org/wiki/Fedora_32_Mass_Rebuild 120 | 121 | * Thu Jan 2 2020 Oliver Gutierrez - 0.15.0-2 122 | - Removal of python2 dependencies for Fedora 123 | 124 | * Mon Dec 23 2019 Oliver Gutierrez - 0.15.0-1 125 | - Added Firefox bookmarks deployment 126 | 127 | * Mon Oct 21 2019 Miro Hrončok - 0.14.0-3 128 | - Drop requirement of python2-gobject on Fedora 129 | 130 | * Thu Jul 25 2019 Fedora Release Engineering - 0.14.0-2 131 | - Rebuilt for https://fedoraproject.org/wiki/Fedora_31_Mass_Rebuild 132 | 133 | * Mon Jul 15 2019 Oliver Gutierrez - 0.14.0-1 134 | - Added Active Directory support 135 | 136 | * Thu Jan 31 2019 Fedora Release Engineering - 0.10.2-4 137 | - Rebuilt for https://fedoraproject.org/wiki/Fedora_30_Mass_Rebuild 138 | 139 | * Fri Jul 13 2018 Fedora Release Engineering - 0.10.2-3 140 | - Rebuilt for https://fedoraproject.org/wiki/Fedora_29_Mass_Rebuild 141 | 142 | * Wed Apr 11 2018 Oliver Gutierrez - 0.10.2-2 143 | - Fixed building dependencies 144 | 145 | * Wed Apr 11 2018 Oliver Gutierrez - 0.10.2-1 146 | - Updated package for release 0.10.2 147 | 148 | * Thu Mar 01 2018 Iryna Shcherbina - 0.10.0-4 149 | - Update Python 2 dependency declarations to new packaging standards 150 | (See https://fedoraproject.org/wiki/FinalizingFedoraSwitchtoPython3) 151 | 152 | * Wed Feb 07 2018 Fedora Release Engineering - 0.10.0-3 153 | - Rebuilt for https://fedoraproject.org/wiki/Fedora_28_Mass_Rebuild 154 | 155 | * Wed Jul 26 2017 Fedora Release Engineering - 0.10.0-2 156 | - Rebuilt for https://fedoraproject.org/wiki/Fedora_27_Mass_Rebuild 157 | 158 | * Mon Jul 10 2017 Oliver Gutierrez - 0.10.0-1 159 | - Updated package for release 0.10.0 160 | 161 | * Mon Jul 10 2017 Oliver Gutierrez - 0.9.1-1 162 | - Code migration to Python 163 | - Updated package for release 0.9.1 164 | 165 | * Fri Sep 16 2016 Alberto Ruiz - 0.8.0-1 166 | - new version 167 | 168 | * Wed Jul 20 2016 Alberto Ruiz - 0.7.1-1 169 | - This release fixes a regression with systemd autostarting the service once 170 | enabled 171 | 172 | * Wed Feb 03 2016 Alberto Ruiz - 0.7.0-2 173 | - Fix documentation string 174 | 175 | * Tue Jan 19 2016 Alberto Ruiz - 0.7.0-1 176 | - Update package for 0.7.0 177 | 178 | * Fri Jan 15 2016 Alberto Ruiz - 0.3.0-1 179 | - Initial RPM package 180 | -------------------------------------------------------------------------------- /src/fleetcommanderclient/adapters/dconf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # vi:ts=4 sw=4 sts=4 3 | 4 | # Copyright (C) 2019 Red Hat, Inc. 5 | # 6 | # This program is free software; you can redistribute it and/or 7 | # modify it under the terms of the GNU Lesser General Public 8 | # License as published by the Free Software Foundation; either 9 | # version 2.1 of the licence, or (at your option) any later version. 10 | # 11 | # This program is distributed in the hope that it will be useful, 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 14 | # Lesser General Public License for more details. 15 | # 16 | # You should have received a copy of the GNU Lesser General Public 17 | # License along with this program; if not, see . 18 | # 19 | # Authors: Alberto Ruiz 20 | # Oliver Gutiérrez 21 | 22 | import os 23 | import subprocess 24 | import logging 25 | import shutil 26 | 27 | from gi.repository import GLib 28 | 29 | from fleetcommanderclient.adapters.base import BaseAdapter 30 | 31 | 32 | class DconfAdapter(BaseAdapter): 33 | """ 34 | DConf configuration adapter class 35 | """ 36 | 37 | # Namespace this config adapter handles 38 | NAMESPACE = "org.gnome.gsettings" 39 | 40 | PROFILE_FILE = "fleet-commander-dconf.conf" 41 | DB_FILE = "fleet-commander-dconf.db" 42 | 43 | def __init__(self, dconf_profile_path, dconf_db_path): 44 | self.dconf_profile_path = dconf_profile_path 45 | self.dconf_db_path = dconf_db_path 46 | 47 | def _get_paths_for_uid(self, uid): 48 | struid = str(uid) 49 | profile_path = os.path.join(self.dconf_profile_path, struid) 50 | keyfile_dir = os.path.join( 51 | self.dconf_db_path, "{}-{}.d".format(self.DB_FILE, struid) 52 | ) 53 | db_path = os.path.join(self.dconf_db_path, "{}-{}".format(self.DB_FILE, struid)) 54 | return (profile_path, keyfile_dir, db_path) 55 | 56 | def _compile_dconf_db(self, keyfiles_dir, db_file): 57 | """ 58 | Compiles dconf database 59 | """ 60 | # Execute dbus service 61 | with subprocess.Popen( 62 | [ 63 | "dconf", 64 | "compile", 65 | db_file, 66 | keyfiles_dir, 67 | ], 68 | stdout=subprocess.PIPE, 69 | stderr=subprocess.PIPE, 70 | ) as cmd: 71 | cmd.wait() 72 | out = cmd.stdout.read() 73 | err = cmd.stderr.read() 74 | cmd.stdout.close() 75 | cmd.stderr.close() 76 | if cmd.returncode != 0: 77 | raise Exception("{}\n{}".format(out, err)) 78 | 79 | def _remove_path(self, path, throw=False): 80 | logging.debug("Removing path: %s", path) 81 | try: 82 | if os.path.exists(path): 83 | if os.path.isdir(path): 84 | shutil.rmtree(path) 85 | else: 86 | os.remove(path) 87 | except Exception as e: 88 | if throw: 89 | logging.error("Error removing path %s: %s", path, e) 90 | raise e 91 | logging.warning("Error removing path %s: %s", path, e) 92 | 93 | def process_config_data(self, config_data, cache_path): 94 | """ 95 | Process configuration data and save cache files to be deployed. 96 | This method needs to be defined by each configuration adapter. 97 | """ 98 | 99 | # Create keyfile path 100 | keyfiles_dir = os.path.join(cache_path, "keyfiles") 101 | logging.debug("Creating keyfiles directory %s", keyfiles_dir) 102 | try: 103 | os.makedirs(keyfiles_dir) 104 | except Exception as e: 105 | logging.error("Error creating keyfiles path %s: %s", keyfiles_dir, e) 106 | return 107 | 108 | # Prepare data for saving it in keyfile 109 | logging.debug("Preparing dconf data for saving to keyfile") 110 | keyfile = GLib.KeyFile.new() 111 | for item in config_data: 112 | if "key" in item and "value" in item: 113 | keysplit = item["key"][1:].split("/") 114 | keypath = "/".join(keysplit[:-1]) 115 | keyname = keysplit[-1] 116 | keyfile.set_string(keypath, keyname, item["value"]) 117 | 118 | # Save keyfile 119 | keyfile_path = os.path.join(keyfiles_dir, self.PROFILE_FILE) 120 | logging.debug("Saving dconf keyfile to %s", keyfile_path) 121 | try: 122 | keyfile.save_to_file(keyfile_path) 123 | except Exception as e: 124 | logging.error('Error saving dconf keyfile at "%s": %s', keyfile_path, e) 125 | return 126 | 127 | # Compile dconf database 128 | db_path = os.path.join(cache_path, self.DB_FILE) 129 | try: 130 | self._compile_dconf_db(keyfiles_dir, db_path) 131 | except Exception as e: 132 | logging.error("Error compiling dconf data to %s: %s", cache_path, e) 133 | return 134 | 135 | def deploy_files(self, cache_path, uid): 136 | """ 137 | Copy cached policies file to policies directory 138 | This method will be called by privileged process 139 | """ 140 | 141 | cached_db_file_path = os.path.join(cache_path, self.DB_FILE) 142 | 143 | if os.path.isfile(cached_db_file_path): 144 | logging.debug( 145 | "Deploying dconf settings from database file %s", cached_db_file_path 146 | ) 147 | 148 | profile_path, keyfile_dir, db_path = self._get_paths_for_uid(uid) 149 | 150 | # Remove old paths 151 | for path in [profile_path, keyfile_dir, db_path]: 152 | self._remove_path(path) 153 | 154 | # Create runtime path 155 | logging.debug("Creating profile path for dconf %s", profile_path) 156 | try: 157 | os.makedirs(self.dconf_profile_path) 158 | except Exception: 159 | pass 160 | 161 | # Copy db file from cache to db path 162 | deploy_db_file_path = os.path.join( 163 | self.dconf_db_path, "{}-{}".format(self.DB_FILE, uid) 164 | ) 165 | shutil.copyfile(cached_db_file_path, deploy_db_file_path) 166 | 167 | # Save runtime file 168 | try: 169 | profile_data = "user-db:user\n\nsystem-db:{}-{}".format( 170 | self.DB_FILE, uid 171 | ) 172 | with open(profile_path, "w") as fd: 173 | fd.write(profile_data) 174 | fd.close() 175 | except Exception as e: 176 | logging.error("Error saving dconf profile at %s: %s", profile_path, e) 177 | return 178 | 179 | logging.info("Processed dconf configuration for UID %s", uid) 180 | 181 | # # Change permissions and ownership for accounts file 182 | # os.chown(deploy_file_path, uid, -1) 183 | # os.chmod(deploy_file_path, stat.S_IREAD) 184 | 185 | # # Change permissions and ownership for GOA runtime directory 186 | # os.chown(runtime_path, uid, -1) 187 | # os.chmod(runtime_path, stat.S_IREAD | stat.S_IEXEC) 188 | 189 | else: 190 | logging.debug( 191 | "Dconf settings database file %s not present. Ignoring.", 192 | cached_db_file_path, 193 | ) 194 | -------------------------------------------------------------------------------- /tests/04_configadapter_nm.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | # vi:ts=2 sw=2 sts=2 4 | 5 | # Copyright (C) 2017 Red Hat, Inc. 6 | # 7 | # This program is free software; you can redistribute it and/or 8 | # modify it under the terms of the GNU Lesser General Public 9 | # License as published by the Free Software Foundation; either 10 | # version 2.1 of the licence, or (at your option) any later version. 11 | # 12 | # This program is distributed in the hope that it will be useful, 13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 15 | # Lesser General Public License for more details. 16 | # 17 | # You should have received a copy of the GNU Lesser General Public 18 | # License along with this program; if not, see . 19 | # 20 | # Authors: Alberto Ruiz 21 | # Oliver Gutiérrez 22 | 23 | import os 24 | import sys 25 | import unittest 26 | import uuid 27 | import logging 28 | 29 | import dbus.service 30 | import dbus.mainloop.glib 31 | 32 | if sys.version_info < (3,): 33 | import mock 34 | else: 35 | from unittest import mock 36 | 37 | import dbusmock 38 | from dbusmock.templates.networkmanager import ( 39 | MANAGER_IFACE, 40 | SETTINGS_OBJ, 41 | SETTINGS_IFACE, 42 | ) 43 | 44 | sys.path.append(os.path.join(os.environ["TOPSRCDIR"], "src")) 45 | 46 | from fleetcommanderclient.configadapters import NetworkManagerConfigAdapter 47 | 48 | 49 | # Set log level to debug 50 | logging.basicConfig(level=logging.DEBUG) 51 | 52 | 53 | USER_NAME = "myuser" 54 | 55 | 56 | def mocked_uname(uid): 57 | """ 58 | This is a mock for os.pwd.getpwuid 59 | """ 60 | 61 | class MockPwd: 62 | pw_name = USER_NAME 63 | 64 | if uid == 55555: 65 | return MockPwd() 66 | raise Exception("Unknown UID: %d" % uid) 67 | 68 | 69 | class TestNetworkManagerConfigAdapter(dbusmock.DBusTestCase): 70 | TEST_UID = 55555 71 | TEST_DATA = [ 72 | { 73 | "data": "{'connection': {'id': <'Company VPN'>, 'uuid': <'601d3b48-a44f-40f3-aa7a-35da4a10a099'>, 'type': <'vpn'>, 'autoconnect': , 'secondaries': <@as []>}, 'ipv6': {'method': <'auto'>, 'dns': <@aay []>, 'dns-search': <@as []>, 'address-data': <@aa{sv} []>, 'route-data': <@aa{sv} []>}, 'ipv4': {'method': <'auto'>, 'dns': <@au []>, 'dns-search': <@as []>, 'address-data': <@aa{sv} []>, 'route-data': <@aa{sv} []>}, 'vpn': {'service-type': <'org.freedesktop.NetworkManager.vpnc'>, 'data': <{'NAT Traversal Mode': 'natt', 'ipsec-secret-type': 'ask', 'IPSec secret-flags': '2', 'xauth-password-type': 'ask', 'Vendor': 'cisco', 'Xauth username': 'vpnusername', 'IPSec gateway': 'vpn.mycompany.com', 'Xauth password-flags': '2', 'IPSec ID': 'vpngroupname', 'Perfect Forward Secrecy': 'server', 'IKE DH Group': 'dh2', 'Local Port': '0'}>, 'secrets': <@a{ss} {}>}}", 74 | "type": "vpn", 75 | "uuid": "601d3b48-a44f-40f3-aa7a-35da4a10a099", 76 | "id": "The Company VPN", 77 | }, 78 | { 79 | "data": "{'connection': {'id': <'Intranet VPN'>, 'uuid': <'0be7d422-1635-11e7-a83f-68f728db19d3'>, 'type': <'vpn'>, 'autoconnect': , 'secondaries': <@as []>}, 'ipv6': {'method': <'auto'>, 'dns': <@aay []>, 'dns-search': <@as []>, 'address-data': <@aa{sv} []>, 'route-data': <@aa{sv} []>}, 'ipv4': {'method': <'auto'>, 'dns': <@au []>, 'dns-search': <@as []>, 'address-data': <@aa{sv} []>, 'route-data': <@aa{sv} []>}, 'vpn': {'service-type': <'org.freedesktop.NetworkManager.vpnc'>, 'data': <{'NAT Traversal Mode': 'natt', 'ipsec-secret-type': 'ask', 'IPSec secret-flags': '2', 'xauth-password-type': 'ask', 'Vendor': 'cisco', 'Xauth username': 'vpnusername', 'IPSec gateway': 'vpn.mycompany.com', 'Xauth password-flags': '2', 'IPSec ID': 'vpngroupname', 'Perfect Forward Secrecy': 'server', 'IKE DH Group': 'dh2', 'Local Port': '0'}>, 'secrets': <@a{ss} {}>}}", 80 | "type": "vpn", 81 | "uuid": "0be7d422-1635-11e7-a83f-68f728db19d3", 82 | "id": "Intranet VPN", 83 | }, 84 | ] 85 | 86 | @classmethod 87 | def setUpClass(cls): 88 | cls.start_system_bus() 89 | cls.dbus_con = cls.get_dbus(True) 90 | 91 | def setUp(self): 92 | self.p_mock, self.obj_nm = self.spawn_server_template( 93 | "networkmanager", {"NetworkingEnabled": True} 94 | ) 95 | self.settings = dbus.Interface( 96 | self.dbus_con.get_object(MANAGER_IFACE, SETTINGS_OBJ), SETTINGS_IFACE 97 | ) 98 | 99 | def tearDown(self): 100 | self.p_mock.terminate() 101 | self.p_mock.wait() 102 | 103 | def test_00_bootstrap(self): 104 | NetworkManagerConfigAdapter().bootstrap(self.TEST_UID) 105 | 106 | @mock.patch("pwd.getpwuid", side_effect=mocked_uname) 107 | def test_01_update(self, side_effect): 108 | uuid1 = "601d3b48-a44f-40f3-aa7a-35da4a10a099" 109 | uuid2 = "0be7d422-1635-11e7-a83f-68f728db19d3" 110 | hashed_uuid1 = str(uuid.uuid5(uuid.UUID(uuid1), USER_NAME)) 111 | hashed_uuid2 = str(uuid.uuid5(uuid.UUID(uuid2), USER_NAME)) 112 | 113 | # We add an existing connection to trigger an Update method 114 | self.settings.AddConnection( 115 | dbus.Dictionary( 116 | { 117 | "connection": dbus.Dictionary( 118 | { 119 | "id": "test connection", 120 | "uuid": hashed_uuid1, 121 | "type": "802-11-wireless", 122 | }, 123 | signature="sv", 124 | ), 125 | "802-11-wireless": dbus.Dictionary( 126 | {"ssid": dbus.ByteArray("The_SSID".encode("UTF-8"))}, 127 | signature="sv", 128 | ), 129 | } 130 | ) 131 | ) 132 | 133 | ca = NetworkManagerConfigAdapter() 134 | ca.bootstrap(self.TEST_UID) 135 | ca.update(self.TEST_UID, self.TEST_DATA) 136 | 137 | conns = self.settings.ListConnections() 138 | 139 | logging.debug("Connections: %s", conns) 140 | 141 | self.assertEqual(len(conns), 2) 142 | 143 | path1 = self.settings.GetConnectionByUuid(hashed_uuid1) 144 | path2 = self.settings.GetConnectionByUuid(hashed_uuid2) 145 | 146 | self.assertIn(path1, conns) 147 | self.assertIn(path2, conns) 148 | 149 | conn1 = dbus.Interface( 150 | self.dbus_con.get_object(MANAGER_IFACE, path1), 151 | "org.freedesktop.NetworkManager.Settings.Connection", 152 | ) 153 | conn2 = dbus.Interface( 154 | self.dbus_con.get_object(MANAGER_IFACE, path2), 155 | "org.freedesktop.NetworkManager.Settings.Connection", 156 | ) 157 | 158 | conn1_sett = conn1.GetSettings() 159 | conn2_sett = conn2.GetSettings() 160 | 161 | self.assertEqual(conn1_sett["connection"]["uuid"], hashed_uuid1) 162 | self.assertEqual(conn2_sett["connection"]["uuid"], hashed_uuid2) 163 | 164 | self.assertEqual( 165 | conn1_sett["connection"]["permissions"], 166 | [ 167 | "user:%s:" % USER_NAME, 168 | ], 169 | ) 170 | self.assertEqual( 171 | conn2_sett["connection"]["permissions"], 172 | [ 173 | "user:%s:" % USER_NAME, 174 | ], 175 | ) 176 | 177 | self.assertEqual( 178 | conn1_sett["user"]["data"]["org.fleet-commander.connection"], "true" 179 | ) 180 | self.assertEqual( 181 | conn1_sett["user"]["data"]["org.fleet-commander.connection.uuid"], uuid1 182 | ) 183 | 184 | self.assertEqual( 185 | conn2_sett["user"]["data"]["org.fleet-commander.connection"], "true" 186 | ) 187 | self.assertEqual( 188 | conn2_sett["user"]["data"]["org.fleet-commander.connection.uuid"], uuid2 189 | ) 190 | 191 | 192 | if __name__ == "__main__": 193 | unittest.main() 194 | --------------------------------------------------------------------------------