├── 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 |
--------------------------------------------------------------------------------