Implement FR #3209: Add native support for authentication via Intune dbus interface (#3274)

* Adds support for calling `acquireTokenInteractively` and `acquireTokenSilently` via D-Bus
* Parses and handles the full `brokerTokenResponse` object returned by the Intune broker
* Stores and reuses `account` data to enable silent token refresh without repeated interactive authentication
* Ensures the access token and its expiry time are properly calculated and stored for consistent token management
* Fallback to interactive authentication is triggered if silent authentication fails
This commit is contained in:
abraunegg 2025-05-26 17:14:50 +10:00 committed by GitHub
commit 61e5a1edb6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
18 changed files with 847 additions and 175 deletions

View file

@ -157,6 +157,7 @@ hnsecs
howto
hskrieg
htons
ietf
idk
idlol
idup
@ -195,6 +196,7 @@ lgio
lglib
lgobject
libcrypto
libdbus
libdir
libexec
libexecdir
@ -280,6 +282,7 @@ onetoc
onlydeps
onmicrosoft
ontextmessage
oob
opensuse
overallocated
pacman

View file

@ -26,7 +26,7 @@ jobs:
run: sudo apt install -y build-essential
- name: Install build-dependencies
run: sudo apt install -y libcurl4-openssl-dev libsqlite3-dev pkg-config git curl ldc
run: sudo apt install -y libcurl4-openssl-dev libsqlite3-dev pkg-config git curl ldc libdbus-1-dev
- name: Configure
run: ./configure

View file

@ -22,7 +22,7 @@ NOTIFICATIONS = @NOTIFICATIONS@
HAVE_SYSTEMD = @HAVE_SYSTEMD@
systemduserunitdir = @systemduserunitdir@
systemdsystemunitdir = @systemdsystemunitdir@
all_libs = @curl_LIBS@ @sqlite_LIBS@ @notify_LIBS@ @bsd_inotify_LIBS@ @dynamic_linker_LIBS@
all_libs = @curl_LIBS@ @sqlite_LIBS@ @dbus_LIBS@ @notify_LIBS@ @bsd_inotify_LIBS@ @dynamic_linker_LIBS@
COMPLETIONS = @COMPLETIONS@
BASH_COMPLETION_DIR = @BASH_COMPLETION_DIR@
ZSH_COMPLETION_DIR = @ZSH_COMPLETION_DIR@
@ -77,7 +77,8 @@ SOURCES = \
src/clientSideFiltering.d \
src/monitor.d \
src/arsd/cgi.d \
src/xattr.d
src/xattr.d \
src/intune.d
ifeq ($(NOTIFICATIONS),yes)
SOURCES += src/notifications/notify.d src/notifications/dnotify.d

3
config
View file

@ -178,6 +178,9 @@
## Only upload changes to OneDrive, do not download from cloud.
#upload_only = "false"
## Single Sign-On (SSO) via Intune using the Microsoft Identity Device Broker
#use_intune_sso = "true"
## This configuration option controls the application function to move online deleted files to a 'Recycle Bin' on your system.
#use_recycle_bin = "false"

119
configure vendored
View file

@ -601,6 +601,9 @@ notify_CFLAGS
HAVE_SYSTEMD
systemduserunitdir
systemdsystemunitdir
enable_dbus
dbus_LIBS
dbus_CFLAGS
sqlite_LIBS
sqlite_CFLAGS
curl_LIBS
@ -639,6 +642,7 @@ infodir
docdir
oldincludedir
includedir
runstatedir
localstatedir
sharedstatedir
sysconfdir
@ -683,6 +687,8 @@ curl_CFLAGS
curl_LIBS
sqlite_CFLAGS
sqlite_LIBS
dbus_CFLAGS
dbus_LIBS
notify_CFLAGS
notify_LIBS
bashcompdir'
@ -724,6 +730,7 @@ datadir='${datarootdir}'
sysconfdir='${prefix}/etc'
sharedstatedir='${prefix}/com'
localstatedir='${prefix}/var'
runstatedir='${localstatedir}/run'
includedir='${prefix}/include'
oldincludedir='/usr/include'
docdir='${datarootdir}/doc/${PACKAGE_TARNAME}'
@ -976,6 +983,15 @@ do
| -silent | --silent | --silen | --sile | --sil)
silent=yes ;;
-runstatedir | --runstatedir | --runstatedi | --runstated \
| --runstate | --runstat | --runsta | --runst | --runs \
| --run | --ru | --r)
ac_prev=runstatedir ;;
-runstatedir=* | --runstatedir=* | --runstatedi=* | --runstated=* \
| --runstate=* | --runstat=* | --runsta=* | --runst=* | --runs=* \
| --run=* | --ru=* | --r=*)
runstatedir=$ac_optarg ;;
-sbindir | --sbindir | --sbindi | --sbind | --sbin | --sbi | --sb)
ac_prev=sbindir ;;
-sbindir=* | --sbindir=* | --sbindi=* | --sbind=* | --sbin=* \
@ -1113,7 +1129,7 @@ fi
for ac_var in exec_prefix prefix bindir sbindir libexecdir datarootdir \
datadir sysconfdir sharedstatedir localstatedir includedir \
oldincludedir docdir infodir htmldir dvidir pdfdir psdir \
libdir localedir mandir
libdir localedir mandir runstatedir
do
eval ac_val=\$$ac_var
# Remove trailing slashes.
@ -1266,6 +1282,7 @@ Fine tuning of the installation directories:
--sysconfdir=DIR read-only single-machine data [PREFIX/etc]
--sharedstatedir=DIR modifiable architecture-independent data [PREFIX/com]
--localstatedir=DIR modifiable single-machine data [PREFIX/var]
--runstatedir=DIR modifiable per-process data [LOCALSTATEDIR/run]
--libdir=DIR object code libraries [EPREFIX/lib]
--includedir=DIR C header files [PREFIX/include]
--oldincludedir=DIR C header files for non-gcc [/usr/include]
@ -1328,6 +1345,8 @@ Some influential environment variables:
sqlite_CFLAGS
C compiler flags for sqlite, overriding pkg-config
sqlite_LIBS linker flags for sqlite, overriding pkg-config
dbus_CFLAGS C compiler flags for dbus, overriding pkg-config
dbus_LIBS linker flags for dbus, overriding pkg-config
notify_CFLAGS
C compiler flags for notify, overriding pkg-config
notify_LIBS linker flags for notify, overriding pkg-config
@ -2014,7 +2033,7 @@ $as_echo "no" >&6; }
fi
fi
PACKAGE_DATE="April 2025"
PACKAGE_DATE="May 2025"
for ac_prog in dmd ldmd2 ldc2 gdmd gdc
@ -2402,6 +2421,101 @@ $as_echo "yes" >&6; }
fi
{ $as_echo "$as_me:${as_lineno-$LINENO}: checking whether to enable dbus support" >&5
$as_echo_n "checking whether to enable dbus support... " >&6; }
case "$(uname -s)" in
Linux)
enable_dbus=yes
{ $as_echo "$as_me:${as_lineno-$LINENO}: result: yes (on Linux)" >&5
$as_echo "yes (on Linux)" >&6; }
pkg_failed=no
{ $as_echo "$as_me:${as_lineno-$LINENO}: checking for dbus" >&5
$as_echo_n "checking for dbus... " >&6; }
if test -n "$dbus_CFLAGS"; then
pkg_cv_dbus_CFLAGS="$dbus_CFLAGS"
elif test -n "$PKG_CONFIG"; then
if test -n "$PKG_CONFIG" && \
{ { $as_echo "$as_me:${as_lineno-$LINENO}: \$PKG_CONFIG --exists --print-errors \"dbus-1 >= 1.0\""; } >&5
($PKG_CONFIG --exists --print-errors "dbus-1 >= 1.0") 2>&5
ac_status=$?
$as_echo "$as_me:${as_lineno-$LINENO}: \$? = $ac_status" >&5
test $ac_status = 0; }; then
pkg_cv_dbus_CFLAGS=`$PKG_CONFIG --cflags "dbus-1 >= 1.0" 2>/dev/null`
test "x$?" != "x0" && pkg_failed=yes
else
pkg_failed=yes
fi
else
pkg_failed=untried
fi
if test -n "$dbus_LIBS"; then
pkg_cv_dbus_LIBS="$dbus_LIBS"
elif test -n "$PKG_CONFIG"; then
if test -n "$PKG_CONFIG" && \
{ { $as_echo "$as_me:${as_lineno-$LINENO}: \$PKG_CONFIG --exists --print-errors \"dbus-1 >= 1.0\""; } >&5
($PKG_CONFIG --exists --print-errors "dbus-1 >= 1.0") 2>&5
ac_status=$?
$as_echo "$as_me:${as_lineno-$LINENO}: \$? = $ac_status" >&5
test $ac_status = 0; }; then
pkg_cv_dbus_LIBS=`$PKG_CONFIG --libs "dbus-1 >= 1.0" 2>/dev/null`
test "x$?" != "x0" && pkg_failed=yes
else
pkg_failed=yes
fi
else
pkg_failed=untried
fi
if test $pkg_failed = yes; then
{ $as_echo "$as_me:${as_lineno-$LINENO}: result: no" >&5
$as_echo "no" >&6; }
if $PKG_CONFIG --atleast-pkgconfig-version 0.20; then
_pkg_short_errors_supported=yes
else
_pkg_short_errors_supported=no
fi
if test $_pkg_short_errors_supported = yes; then
dbus_PKG_ERRORS=`$PKG_CONFIG --short-errors --print-errors --cflags --libs "dbus-1 >= 1.0" 2>&1`
else
dbus_PKG_ERRORS=`$PKG_CONFIG --print-errors --cflags --libs "dbus-1 >= 1.0" 2>&1`
fi
# Put the nasty error message in config.log where it belongs
echo "$dbus_PKG_ERRORS" >&5
as_fn_error $? "dbus-1 development files not found. Please install dbus-devel (Red Hat), libdbus-1-dev (Debian) or dbus (Arch | Manjaro)" "$LINENO" 5
elif test $pkg_failed = untried; then
{ $as_echo "$as_me:${as_lineno-$LINENO}: result: no" >&5
$as_echo "no" >&6; }
as_fn_error $? "dbus-1 development files not found. Please install dbus-devel (Red Hat), libdbus-1-dev (Debian) or dbus (Arch | Manjaro)" "$LINENO" 5
else
dbus_CFLAGS=$pkg_cv_dbus_CFLAGS
dbus_LIBS=$pkg_cv_dbus_LIBS
{ $as_echo "$as_me:${as_lineno-$LINENO}: result: yes" >&5
$as_echo "yes" >&6; }
$as_echo "#define HAVE_DBUS 1" >>confdefs.h
fi
;;
*)
enable_dbus=no
{ $as_echo "$as_me:${as_lineno-$LINENO}: result: no (not on Linux)" >&5
$as_echo "no (not on Linux)" >&6; }
;;
esac
# Check whether --with-systemdsystemunitdir was given.
if test "${with_systemdsystemunitdir+set}" = set; then :
@ -2596,6 +2710,7 @@ esac
# Check whether --enable-completions was given.
if test "${enable_completions+set}" = set; then :
enableval=$enable_completions;

View file

@ -21,7 +21,7 @@ dnl necessary programs: install, pkg-config
AC_PROG_INSTALL
PKG_PROG_PKG_CONFIG
PACKAGE_DATE="April 2025"
PACKAGE_DATE="May 2025"
AC_SUBST([PACKAGE_DATE])
dnl Determine D compiler
@ -185,10 +185,31 @@ AC_SUBST([LINKER_DCFLAG])
AC_SUBST([OUTPUT_DCFLAG])
AC_SUBST([WERROR_DCFLAG])
dnl Check for required modules: curl and sqlite at the moment
dnl Check for required modules: curl, sqlite and dbus if required
PKG_CHECK_MODULES([curl],[libcurl])
PKG_CHECK_MODULES([sqlite],[sqlite3])
AC_MSG_CHECKING([whether to enable dbus support])
case "$(uname -s)" in
Linux)
enable_dbus=yes
AC_MSG_RESULT([yes (on Linux)])
PKG_CHECK_MODULES([dbus], [dbus-1 >= 1.0],
[AC_DEFINE([HAVE_DBUS], [1], [Define if you have dbus-1])]
,
[AC_MSG_ERROR([dbus-1 development files not found. Please install dbus-devel (Red Hat), libdbus-1-dev (Debian) or dbus (Arch | Manjaro)])]
)
;;
*)
enable_dbus=no
AC_MSG_RESULT([no (not on Linux)])
;;
esac
AC_SUBST([enable_dbus])
dnl
dnl systemd and unit file directories
dnl This is a bit tricky, because we want to allow for
@ -304,6 +325,7 @@ esac
AC_SUBST([dynamic_linker_LIBS])
dnl
dnl Completion support
dnl First determine whether completions are requested, pass that to Makefile

View file

@ -11,7 +11,7 @@ RUN go install -ldflags "-s -w" github.com/tianon/gosu@${GOSU_VERSION}
FROM fedora:${FEDORA_VERSION} AS builder-onedrive
RUN dnf install -y ldc pkgconf libcurl-devel sqlite-devel git awk
RUN dnf install -y ldc pkgconf libcurl-devel sqlite-devel dbus-devel git awk
ENV PKG_CONFIG=/usr/bin/pkgconf

View file

@ -10,7 +10,7 @@ RUN go install -ldflags "-s -w" github.com/tianon/gosu@${GOSU_VERSION}
FROM alpine:${ALPINE_VERSION} AS builder-onedrive
RUN apk add --update --no-cache alpine-sdk gnupg xz curl-dev sqlite-dev binutils-gold autoconf automake ldc
RUN apk add --update --no-cache alpine-sdk gnupg xz curl-dev sqlite-dev dbus-dev binutils-gold autoconf automake ldc
COPY . /usr/src/onedrive
WORKDIR /usr/src/onedrive

View file

@ -10,7 +10,7 @@ RUN apt-get clean \
&& apt-get update \
&& apt-get upgrade -y \
&& apt-get update \
&& DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends build-essential curl ca-certificates libcurl4-openssl-dev libsqlite3-dev libxml2-dev pkg-config git ldc \
&& DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends build-essential curl ca-certificates libcurl4-openssl-dev libsqlite3-dev libxml2-dev libdbus-1-dev pkg-config git ldc \
# Install|update curl from backports
&& apt-get install -t bookworm-backports -y curl \
&& rm -rf /var/lib/apt/lists/*

View file

@ -64,6 +64,7 @@ Before reading this document, please ensure you are running application version
- [threads](#threads)
- [transfer_order](#transfer_order)
- [upload_only](#upload_only)
- [use_intune_sso](#use_intune_sso)
- [use_recycle_bin](#use_recycle_bin)
- [user_agent](#user_agent)
- [webhook_enabled](#webhook_enabled)
@ -1017,6 +1018,20 @@ _**CLI Option Use:**_ `--upload-only`
> [!IMPORTANT]
> To ensure that data deleted locally remains accessible online, you can use the 'no_remote_delete' option. If you want to delete the data from your local storage after a successful upload to Microsoft OneDrive, you can use the 'remove_source_files' option.
### use_intune_sso
_**Description:**_ Enable this option to authenticate using Intune Single Sign-On (SSO) via the Microsoft Identity Device Broker over D-Bus. This method is suitable for environments where the system is Intune-enrolled and allows seamless token retrieval without requiring browser interaction.
_**Value Type:**_ Boolean
_**Default Value:**_ False
_**Config Example:**_ `use_intune_sso = "false"` or `use_intune_sso = "true"`
_**CLI Option Use:**_ *None - this is a config file option only*
> [!NOTE]
> The installation and configuration of Intune for your platform is beyond the scope of this documentation.
### use_recycle_bin
_**Description:**_ This configuration option controls the application function to move online deleted files to a 'Recycle Bin' on your system. This allows you to review online deleted data manually before this is purged from your actual system.

View file

@ -60,7 +60,7 @@ As stated above, you will need at least GDC version 15. If your distribution's r
### Dependencies: Arch Linux & Manjaro Linux
```text
sudo pacman -S git make pkg-config curl sqlite ldc
sudo pacman -S git make pkg-config curl sqlite dbus ldc
```
For GUI notifications the following is also necessary:
```text
@ -76,7 +76,7 @@ CentOS 7.x and RHEL 7.x reached End of Life status on June 30th 2024 and is no l
### Dependencies: Fedora > Version 18 / CentOS 8.x / CentOS 9.x / RHEL 8.x / RHEL 9.x
```text
sudo dnf groupinstall 'Development Tools'
sudo dnf install libcurl-devel sqlite-devel
sudo dnf install libcurl-devel sqlite-devel dbus-devel
curl -fsS https://dlang.org/install.sh | bash -s dmd
```
For GUI notifications the following is also necessary:
@ -87,7 +87,7 @@ sudo dnf install libnotify-devel
Fedora > 41 uses dnf5 which removes some deprecated aliases, specifically 'groupinstall' in this instance.
```text
sudo dnf group install development-tools
sudo dnf install libcurl-devel sqlite-devel
sudo dnf install libcurl-devel sqlite-devel dbus-devel
```
Before running the dmd install you need to check for the option 'use-keyboxd' in your gnupg common.conf file and comment it out while running the install.
```text
@ -144,7 +144,7 @@ These dependencies are also applicable for all Ubuntu based distributions such a
* Peppermint OS
```text
sudo apt install build-essential
sudo apt install libcurl4-openssl-dev libsqlite3-dev pkg-config git curl systemd-dev
sudo apt install libcurl4-openssl-dev libsqlite3-dev pkg-config git curl systemd-dev libdbus-1-dev
curl -fsS https://dlang.org/install.sh | bash -s dmd
```
For GUI notifications the following is also necessary:
@ -166,7 +166,7 @@ These instructions were validated using:
```text
sudo apt install build-essential
sudo apt install libcurl4-openssl-dev libsqlite3-dev pkg-config git curl ldc systemd-dev
sudo apt install libcurl4-openssl-dev libsqlite3-dev pkg-config git curl ldc systemd-dev libdbus-1-dev
```
For GUI notifications the following is also necessary:
```text
@ -177,7 +177,7 @@ sudo apt install libnotify-dev
```text
sudo zypper addrepo https://download.opensuse.org/repositories/devel:languages:D/openSUSE_Leap_15.0/devel:languages:D.repo
sudo zypper refresh
sudo zypper install gcc git libcurl-devel sqlite3-devel dmd phobos-devel phobos-devel-static
sudo zypper install gcc git libcurl-devel sqlite3-devel dmd phobos-devel phobos-devel-static dbus-1-devel
```
For GUI notifications the following is also necessary:
```text
@ -188,7 +188,7 @@ sudo zypper install libnotify-devel
```text
sudo zypper addrepo https://download.opensuse.org/repositories/devel:languages:D/openSUSE_Leap_15.1/devel:languages:D.repo
sudo zypper refresh
sudo zypper install gcc git libcurl-devel sqlite3-devel dmd phobos-devel phobos-devel-static
sudo zypper install gcc git libcurl-devel sqlite3-devel dmd phobos-devel phobos-devel-static dbus-1-devel
```
For GUI notifications the following is also necessary:
```text
@ -198,7 +198,7 @@ sudo zypper install libnotify-devel
### Dependencies: OpenSuSE Leap 15.2
```text
sudo zypper refresh
sudo zypper install gcc git libcurl-devel sqlite3-devel dmd phobos-devel phobos-devel-static
sudo zypper install gcc git libcurl-devel sqlite3-devel dmd phobos-devel phobos-devel-static dbus-1-devel
```
For GUI notifications the following is also necessary:
```text

View file

@ -341,6 +341,11 @@ Once you've installed the application, you'll need to authorise it using your Mi
Please be aware that some companies may require you to explicitly add this app to the [Microsoft MyApps portal](https://myapps.microsoft.com/). To add an approved app to your apps, click on the ellipsis in the top-right corner and select "Request new apps." On the next page, you can add this app. If it's not listed, you should make a request through your IT department.
This client supports the following methods to authenticate the application with Microsoft OneDrive:
* Supports interactive browser-based authentication using OAuth2 and a response URI
* Supports seamless Single Sign-On (SSO) via Intune using the Microsoft Identity Device Broker D-Bus interface
#### Interactive Authentication using OAuth2 and a response URI
When you run the application for the first time, you'll be prompted to open a specific URL using your web browser, where you'll need to log in to your Microsoft Account and grant the application permission to access your files. After granting permission to the application, you'll be redirected to a blank page. Simply copy the URI from the blank page and paste it into the application.
**Example:**
@ -348,7 +353,7 @@ When you run the application for the first time, you'll be prompted to open a sp
[user@hostname ~]$ onedrive
Authorise this app by visiting:
https://login.microsoftonline.com/common/oauth2/v2.0/authorise?client_id=22c49a0d-d21c-4792-aed1-8f163c982546&scope=Files.ReadWrite%20Files.ReadWrite.all%20Sites.ReadWrite.All%20offline_access&response_type=code&redirect_uri=https://login.microsoftonline.com/common/oauth2/nativeclient
https://login.microsoftonline.com/common/oauth2/v2.0/authorise?client_id=d50ca740-c83f-4d1b-b616-12c519384f0c&scope=Files.ReadWrite%20Files.ReadWrite.all%20Sites.ReadWrite.All%20offline_access&response_type=code&redirect_uri=https://login.microsoftonline.com/common/oauth2/nativeclient
Enter the response URI from your browser: https://login.microsoftonline.com/common/oauth2/nativeclient?code=<redacted>
@ -360,6 +365,22 @@ Please use 'onedrive --help' for further assistance on how to run this applicati
> [!IMPORTANT]
> Without additional input or configuration, the OneDrive Client for Linux will automatically adhere to default application settings during synchronisation processes with Microsoft OneDrive.
#### Single Sign-On (SSO) via Intune using the Microsoft Identity Device Broker
To use this method of authentication, you must add the following configuration to your 'config' file:
```
use_intune_sso = "true"
```
The application will check to ensure that Intune is operational and that the required dbus elements are available. Should these be available, the following will be displayed:
```
...
Client has been configured to use Intune SSO via Microsoft Identity Broker dbus session - checking usage criteria
Intune SSO via Microsoft Identity Broker dbus session usage criteria met - will attempt to authenticate via Intune
...
```
> [!NOTE]
> The installation and configuration of Intune for your platform is beyond the scope of this documentation.
### Display Your Applicable Runtime Configuration
To verify the configuration that the application will use, use the following command:
```text

View file

@ -14,21 +14,23 @@ Originally derived as a 'fork' from the [skilion](https://github.com/skilion/one
This client represents a 100% re-imagining of the original work, addressing numerous notable bugs and issues while incorporating a significant array of new features. This client has been under active development since mid-2018.
## Features
* Compatible with OneDrive Personal, OneDrive for Business including accessing Microsoft SharePoint Libraries
* Provides rules for client-side filtering to select data for syncing with Microsoft OneDrive accounts
* Caches sync state for efficiency
* Supports a dry-run option for safe configuration testing
* Validates file transfers to ensure data integrity
* Monitors local files in real-time using inotify
* Supports interrupted uploads for completion at a later time
* Capability to sync remote updates immediately via webhooks
* Enhanced synchronisation speed with multi-threaded file transfers
* Manages traffic bandwidth use with rate limiting
* Supports seamless access to shared folders and files across both OneDrive Personal and OneDrive for Business accounts
* Supports national cloud deployments including Microsoft Cloud for US Government, Microsoft Cloud Germany and Azure and Office 365 operated by VNET in China
* Supports sending desktop alerts using libnotify
* Protects against significant data loss on OneDrive after configuration changes
* Works with both single and multi-tenant applications
* Compatible with OneDrive Personal and OneDrive for Business, including access to Microsoft SharePoint Libraries
* Supports seamless access to shared folders and files across both OneDrive Personal and OneDrive for Business accounts
* Supports single-tenant and multi-tenant applications
* Supports Intune Single Sign-On (SSO) authentication via the Microsoft Identity Device Broker (D-Bus interface)
* Supports national cloud deployments including Microsoft Cloud for US Government, Microsoft Cloud Germany, and Azure/Office 365 operated by VNET in China
* Provides rules for client-side filtering to select data for syncing with Microsoft OneDrive accounts
* Protects against significant data loss on OneDrive after configuration changes
* Supports a dry-run option for safe configuration testing
* Validates file transfers to ensure data integrity
* Caches sync state for efficiency
* Monitors local files in real-time using inotify
* Capability to sync remote updates immediately via webhooks
* Supports interrupted uploads for completion at a later time
* Enhanced synchronisation speed with multi-threaded file transfers
* Manages traffic bandwidth use with rate limiting
* Supports sending desktop alerts using libnotify
## What's missing
* Ability to encrypt/decrypt files on-the-fly when uploading/downloading files from OneDrive

View file

@ -50,6 +50,7 @@ class ApplicationConfig {
// Microsoft Requirements
// - Default Application ID (abraunegg)
immutable string defaultApplicationId = "d50ca740-c83f-4d1b-b616-12c519384f0c";
// - Microsoft User Agent ISV Tag
immutable string isvTag = "ISV";
// - Microsoft User Agent Company name
@ -119,6 +120,10 @@ class ApplicationConfig {
SysTime accessTokenExpiration;
// Store the 'session_upload.CRC32-HASH' file path
string uploadSessionFilePath = "";
// Store the Intune account information
string intuneAccountDetails;
// Store the Intune account information on disk for reuse
string intuneAccountDetailsFilePath = "";
// API initialisation flags
bool apiWasInitialised = false;
@ -412,6 +417,9 @@ class ApplicationConfig {
// Diable setting the permissions for directories and files, using the inherited permissions
boolValues["disable_permission_set"] = false;
// Use authentication via Intune SSO via Microsoft Identity Broker (microsoft-identity-broker) dbus session
boolValues["use_intune_sso"] = false;
// EXPAND USERS HOME DIRECTORY
// Determine the users home directory.
// Need to avoid using ~ here as expandTilde() below does not interpret correctly when running under init.d or systemd scripts
@ -535,6 +543,8 @@ class ApplicationConfig {
// Update application set variables based on configDirName
// - What is the full path for the 'refresh_token'
refreshTokenFilePath = buildNormalizedPath(buildPath(configDirName, "refresh_token"));
// - What is the full path for the 'intune_account'
intuneAccountDetailsFilePath = buildNormalizedPath(buildPath(configDirName, "intune_account"));
// - What is the full path for the 'delta_link'
deltaLinkFilePath = buildNormalizedPath(buildPath(configDirName, "delta_link"));
// - What is the full path for the 'items.sqlite3' - the database cache file
@ -561,17 +571,18 @@ class ApplicationConfig {
// Debug Output for application set variables based on configDirName
if (debugLogging) {
addLogEntry("refreshTokenFilePath = " ~ refreshTokenFilePath, ["debug"]);
addLogEntry("deltaLinkFilePath = " ~ deltaLinkFilePath, ["debug"]);
addLogEntry("databaseFilePath = " ~ databaseFilePath, ["debug"]);
addLogEntry("databaseFilePathDryRun = " ~ databaseFilePathDryRun, ["debug"]);
addLogEntry("uploadSessionFilePath = " ~ uploadSessionFilePath, ["debug"]);
addLogEntry("userConfigFilePath = " ~ userConfigFilePath, ["debug"]);
addLogEntry("syncListFilePath = " ~ syncListFilePath, ["debug"]);
addLogEntry("systemConfigFilePath = " ~ systemConfigFilePath, ["debug"]);
addLogEntry("configBackupFile = " ~ configBackupFile, ["debug"]);
addLogEntry("configHashFile = " ~ configHashFile, ["debug"]);
addLogEntry("syncListHashFile = " ~ syncListHashFile, ["debug"]);
addLogEntry("refreshTokenFilePath = " ~ refreshTokenFilePath, ["debug"]);
addLogEntry("intuneAccountDetailsFilePath = " ~ intuneAccountDetailsFilePath, ["debug"]);
addLogEntry("deltaLinkFilePath = " ~ deltaLinkFilePath, ["debug"]);
addLogEntry("databaseFilePath = " ~ databaseFilePath, ["debug"]);
addLogEntry("databaseFilePathDryRun = " ~ databaseFilePathDryRun, ["debug"]);
addLogEntry("uploadSessionFilePath = " ~ uploadSessionFilePath, ["debug"]);
addLogEntry("userConfigFilePath = " ~ userConfigFilePath, ["debug"]);
addLogEntry("syncListFilePath = " ~ syncListFilePath, ["debug"]);
addLogEntry("systemConfigFilePath = " ~ systemConfigFilePath, ["debug"]);
addLogEntry("configBackupFile = " ~ configBackupFile, ["debug"]);
addLogEntry("configHashFile = " ~ configHashFile, ["debug"]);
addLogEntry("syncListHashFile = " ~ syncListHashFile, ["debug"]);
}
// Configure the Hash and Backup File Permission Value
@ -773,6 +784,13 @@ class ApplicationConfig {
return configuredFilePermissionMode;
}
// Set file permissions for 'refresh_token' and 'intune_account' to 0600
int returnSecureFilePermission() {
string valueToConvert = to!string(defaultFilePermissionMode);
auto convertedValue = parse!long(valueToConvert, 8);
return to!int(convertedValue);
}
// Load a configuration file from the provided filename
private bool loadConfigFile(string filename) {
try {
@ -1537,6 +1555,9 @@ class ApplicationConfig {
// Config Options as per 'config' file
addLogEntry("Config option 'sync_dir' = " ~ getValueString("sync_dir"));
// authentication
addLogEntry("Config option 'use_intune_sso' = " ~ to!string(getValueBool("use_intune_sso")));
// logging and notifications
addLogEntry("Config option 'enable_logging' = " ~ to!string(getValueBool("enable_logging")));
addLogEntry("Config option 'log_dir' = " ~ getValueString("log_dir"));

254
src/intune.d Normal file
View file

@ -0,0 +1,254 @@
// What is this module called?
module intune;
// What does this module require to function?
import core.stdc.string : strcmp;
import core.stdc.stdlib : malloc, free;
import core.thread : Thread;
import core.time : dur;
import std.string : fromStringz, toStringz;
import std.conv : to;
import std.json : JSONValue, parseJSON, JSONType;
import std.uuid : randomUUID;
import std.range : empty;
import std.format : format;
// What 'onedrive' modules do we import?
import log;
extern(C):
alias dbus_bool_t = int;
struct DBusError {
char* name;
char* message;
uint[8] dummy;
void* padding;
}
struct DBusConnection;
struct DBusMessage;
struct DBusMessageIter;
enum DBusBusType {
DBUS_BUS_SESSION = 0,
}
void dbus_error_init(DBusError* error);
void dbus_error_free(DBusError* error);
int dbus_error_is_set(DBusError* error);
DBusConnection* dbus_bus_get(DBusBusType type, DBusError* error);
dbus_bool_t dbus_bus_name_has_owner(DBusConnection* conn, const char* name, DBusError* error);
DBusMessage* dbus_message_new_method_call(const char* dest, const char* path, const char* iface, const char* method);
dbus_bool_t dbus_connection_send(DBusConnection* conn, DBusMessage* msg, void* client_serial);
void dbus_connection_flush(DBusConnection* conn);
DBusMessage* dbus_connection_send_with_reply_and_block(DBusConnection* conn, DBusMessage* msg, int timeout_ms, DBusError* error);
void dbus_message_unref(DBusMessage* msg);
dbus_bool_t dbus_message_iter_init_append(DBusMessage* msg, DBusMessageIter* iter);
dbus_bool_t dbus_message_iter_append_basic(DBusMessageIter* iter, int type, const void* value);
dbus_bool_t dbus_message_iter_init(DBusMessage* msg, DBusMessageIter* iter);
dbus_bool_t dbus_message_iter_get_arg_type(DBusMessageIter* iter);
void dbus_message_iter_get_basic(DBusMessageIter* iter, void* value);
enum DBUS_TYPE_STRING = 115;
enum DBUS_MESSAGE_ITER_SIZE = 128;
bool check_intune_broker_available() {
version (linux) {
DBusError err;
dbus_error_init(&err);
DBusConnection* conn = dbus_bus_get(DBusBusType.DBUS_BUS_SESSION, &err);
if (dbus_error_is_set(&err)) {
dbus_error_free(&err);
return false;
}
if (conn is null) return false;
dbus_bool_t hasOwner = dbus_bus_name_has_owner(conn, "com.microsoft.identity.broker1", &err);
if (dbus_error_is_set(&err)) {
dbus_error_free(&err);
return false;
}
return hasOwner != 0;
} else {
return false;
}
}
bool wait_for_broker(int timeoutSeconds = 10) {
int waited = 0;
while (waited < timeoutSeconds) {
if (check_intune_broker_available()) return true;
Thread.sleep(dur!"seconds"(1));
waited++;
}
return false;
}
string build_auth_request(string accountJson = "", string clientId) {
string header = format(`{
"authParameters": {
"clientId": "%s",
"redirectUri": "https://login.microsoftonline.com/common/oauth2/nativeclient",
"authority": "https://login.microsoftonline.com/common",
"requestedScopes": [
"Files.ReadWrite",
"Files.ReadWrite.All",
"Sites.ReadWrite.All",
"offline_access"
]`, clientId);
string footer = `
}
}`;
if (!accountJson.empty)
return header ~ `,"account": ` ~ accountJson ~ footer;
else
return header ~ footer;
}
struct AuthResult {
JSONValue brokerTokenResponse;
}
// Initiate interactive authentication via D-Bus using the Microsoft Identity Broker
AuthResult acquire_token_interactive(string clientId) {
AuthResult result;
version (linux) {
if (!wait_for_broker(10)) {
addLogEntry("Timed out waiting for Identity Broker to appear on D-Bus");
return result;
}
// Step 1: Call acquireTokenInteractively and capture account from result
DBusError err;
dbus_error_init(&err);
DBusConnection* conn = dbus_bus_get(DBusBusType.DBUS_BUS_SESSION, &err);
if (dbus_error_is_set(&err) || conn is null) return result;
DBusMessage* msg = dbus_message_new_method_call(
"com.microsoft.identity.broker1",
"/com/microsoft/identity/broker1",
"com.microsoft.identity.Broker1",
"acquireTokenInteractively"
);
if (msg is null) return result;
string correlationId = randomUUID().toString();
string accountJson = "";
string requestJson = build_auth_request(accountJson, clientId);
DBusMessageIter* args = cast(DBusMessageIter*) malloc(DBUS_MESSAGE_ITER_SIZE);
if (!dbus_message_iter_init_append(msg, args)) {
dbus_message_unref(msg); free(args); return result;
}
const(char)* protocol = toStringz("0.0");
const(char)* corrId = toStringz(correlationId);
const(char)* reqJson = toStringz(requestJson);
dbus_message_iter_append_basic(args, DBUS_TYPE_STRING, &protocol);
dbus_message_iter_append_basic(args, DBUS_TYPE_STRING, &corrId);
dbus_message_iter_append_basic(args, DBUS_TYPE_STRING, &reqJson);
free(args);
DBusMessage* reply = dbus_connection_send_with_reply_and_block(conn, msg, 60000, &err);
dbus_message_unref(msg);
if (dbus_error_is_set(&err) || reply is null) {
addLogEntry("Interactive call failed");
return result;
}
DBusMessageIter* iter = cast(DBusMessageIter*) malloc(DBUS_MESSAGE_ITER_SIZE);
if (!dbus_message_iter_init(reply, iter)) {
dbus_message_unref(reply); free(iter); return result;
}
char* responseStr;
dbus_message_iter_get_basic(iter, &responseStr);
dbus_message_unref(reply); free(iter);
string jsonResponse = fromStringz(responseStr).idup;
if (debugLogging) {addLogEntry("Interactive raw response: " ~ to!string(jsonResponse), ["debug"]);}
JSONValue parsed = parseJSON(jsonResponse);
if (parsed.type != JSONType.object) return result;
auto obj = parsed.object;
if ("brokerTokenResponse" in obj) {
result.brokerTokenResponse = obj["brokerTokenResponse"];
}
}
return result;
}
// Perform silent authentication via D-Bus using the Microsoft Identity Broker
AuthResult acquire_token_silently(string accountJson, string clientId) {
AuthResult result;
version (linux) {
DBusError err;
dbus_error_init(&err);
DBusConnection* conn = dbus_bus_get(DBusBusType.DBUS_BUS_SESSION, &err);
if (dbus_error_is_set(&err) || conn is null) return result;
DBusMessage* msg = dbus_message_new_method_call(
"com.microsoft.identity.broker1",
"/com/microsoft/identity/broker1",
"com.microsoft.identity.Broker1",
"acquireTokenSilently"
);
if (msg is null) return result;
string correlationId = randomUUID().toString();
string requestJson = build_auth_request(accountJson, clientId);
DBusMessageIter* args = cast(DBusMessageIter*) malloc(DBUS_MESSAGE_ITER_SIZE);
if (!dbus_message_iter_init_append(msg, args)) {
dbus_message_unref(msg);
free(args);
return result;
}
const(char)* protocol = toStringz("0.0");
const(char)* corrId = toStringz(correlationId);
const(char)* reqJson = toStringz(requestJson);
dbus_message_iter_append_basic(args, DBUS_TYPE_STRING, &protocol);
dbus_message_iter_append_basic(args, DBUS_TYPE_STRING, &corrId);
dbus_message_iter_append_basic(args, DBUS_TYPE_STRING, &reqJson);
free(args);
DBusMessage* reply = dbus_connection_send_with_reply_and_block(conn, msg, 10000, &err);
dbus_message_unref(msg);
if (dbus_error_is_set(&err) || reply is null) return result;
DBusMessageIter* iter = cast(DBusMessageIter*) malloc(DBUS_MESSAGE_ITER_SIZE);
if (!dbus_message_iter_init(reply, iter)) {
dbus_message_unref(reply);
free(iter);
return result;
}
char* responseStr;
dbus_message_iter_get_basic(iter, &responseStr);
dbus_message_unref(reply);
free(iter);
string jsonResponse = fromStringz(responseStr).idup;
if (debugLogging) {addLogEntry("Silent raw response: " ~ to!string(jsonResponse), ["debug"]);}
JSONValue parsed = parseJSON(jsonResponse);
if (parsed.type != JSONType.object) return result;
auto obj = parsed.object;
if (!("brokerTokenResponse" in obj)) return result;
result.brokerTokenResponse = obj["brokerTokenResponse"];
}
return result;
}

View file

@ -32,6 +32,7 @@ import itemdb;
import clientSideFiltering;
import monitor;
import webhook;
import intune;
// What other constant variables do we require?
const int EXIT_RESYNC_REQUIRED = 126;
@ -201,6 +202,29 @@ int main(string[] cliArgs) {
syncOrMonitorMissing = true; // --sync or --monitor is missing
}
// Has the client been configured to use Intune SSO via Microsoft Identity Broker (microsoft-identity-broker) dbus session
// This is ONLY possible on Linux, not FreeBSD or other platforms
version (linux) {
if (appConfig.getValueBool("use_intune_sso")) {
// The client is configured to use Intune SSO via Microsoft Identity Broker dbus session
addLogEntry("Client has been configured to use Intune SSO via Microsoft Identity Broker dbus session - checking usage criteria");
// We need to check that the available dbus is actually available
if(wait_for_broker()) {
// Usage criteria met, will attempt to use Intune SSO via dbus
addLogEntry("Intune SSO via Microsoft Identity Broker dbus session usage criteria met - will attempt to authenticate via Intune");
} else {
// Microsoft Identity Broker dbus is not available
addLogEntry();
addLogEntry("Required Microsoft Identity Broker dbus capability not found - disabling authentication via Intune SSO");
addLogEntry();
appConfig.setValueBool("use_intune_sso" , false);
}
}
} else {
// Ensure 'use_intune_sso' is disabled
appConfig.setValueBool("use_intune_sso" , false);
}
// Has the user configured to use the 'Recycle Bin' locally, for any files that are deleted online?
if (appConfig.getValueBool("use_recycle_bin")) {
// Configure the internal application paths which will be used to move rather than delete any online deletes to
@ -437,7 +461,10 @@ int main(string[] cliArgs) {
if (debugLogging) {addLogEntry("--logout requested", ["debug"]);}
addLogEntry("Deleting the saved authentication status ...");
if (!dryRun) {
// Remove the 'refresh_token' file if present
safeRemove(appConfig.refreshTokenFilePath);
// Remove the 'intune_account' file if present
safeRemove(appConfig.intuneAccountDetailsFilePath);
} else {
// --dry-run scenario ... technically we should not be making any local file changes .......
addLogEntry("DRY RUN: Not removing the saved authentication status");
@ -451,7 +478,10 @@ int main(string[] cliArgs) {
if (debugLogging) {addLogEntry("--reauth requested", ["debug"]);}
addLogEntry("Deleting the saved authentication status ... re-authentication requested");
if (!dryRun) {
// Remove the 'refresh_token' file if present
safeRemove(appConfig.refreshTokenFilePath);
// Remove the 'intune_account' file if present
safeRemove(appConfig.intuneAccountDetailsFilePath);
} else {
// --dry-run scenario ... technically we should not be making any local file changes .......
addLogEntry("DRY RUN: Not removing the saved authentication status");

View file

@ -29,6 +29,7 @@ import config;
import log;
import util;
import curlEngine;
import intune;
// Define the 'OneDriveException' class
class OneDriveException: Exception {
@ -310,42 +311,55 @@ class OneDriveApi {
}
// Has the application been authenticated?
if (!exists(appConfig.refreshTokenFilePath)) {
if (debugLogging) {addLogEntry("Application has no 'refresh_token' thus needs to be authenticated", ["debug"]);}
authorised = authorise();
} else {
// Try and read the value from the appConfig if it is set, rather than trying to read the value from disk
if (!appConfig.refreshToken.empty) {
if (debugLogging) {addLogEntry("Read token from appConfig", ["debug"]);}
refreshToken = strip(appConfig.refreshToken);
authorised = true;
// How do we authenticate - standard method or via Intune?
if (appConfig.getValueBool("use_intune_sso")) {
// Authenticate via Intune
if (appConfig.accessToken.empty) {
// No authentication via intune yet
authorised = authorise();
} else {
// Try and read the file from disk
try {
refreshToken = strip(readText(appConfig.refreshTokenFilePath));
// is the refresh_token empty?
if (refreshToken.empty) {
addLogEntry("RefreshToken exists but is empty: " ~ appConfig.refreshTokenFilePath);
authorised = authorise();
} else {
// Existing token not empty
authorised = true;
// update appConfig.refreshToken
appConfig.refreshToken = refreshToken;
}
} catch (FileException exception) {
authorised = authorise();
} catch (std.utf.UTFException exception) {
// path contains characters which generate a UTF exception
addLogEntry("Cannot read refreshToken from: " ~ appConfig.refreshTokenFilePath);
addLogEntry(" Error Reason:" ~ exception.msg);
authorised = false;
}
// We are already authenticated
authorised = true;
}
if (refreshToken.empty) {
// PROBLEM ... CODING TO DO ??????????
if (debugLogging) {addLogEntry("DEBUG: refreshToken is empty !!!!!!!!!!", ["debug"]);}
} else {
// Authenticate via standard method
if (!exists(appConfig.refreshTokenFilePath)) {
if (debugLogging) {addLogEntry("Application has no 'refresh_token' thus needs to be authenticated", ["debug"]);}
authorised = authorise();
} else {
// Try and read the value from the appConfig if it is set, rather than trying to read the value from disk
if (!appConfig.refreshToken.empty) {
if (debugLogging) {addLogEntry("Read token from appConfig", ["debug"]);}
refreshToken = strip(appConfig.refreshToken);
authorised = true;
} else {
// Try and read the file from disk
try {
refreshToken = strip(readText(appConfig.refreshTokenFilePath));
// is the refresh_token empty?
if (refreshToken.empty) {
addLogEntry("RefreshToken exists but is empty: " ~ appConfig.refreshTokenFilePath);
authorised = authorise();
} else {
// Existing token not empty
authorised = true;
// update appConfig.refreshToken
appConfig.refreshToken = refreshToken;
}
} catch (FileException exception) {
authorised = authorise();
} catch (std.utf.UTFException exception) {
// path contains characters which generate a UTF exception
addLogEntry("Cannot read refreshToken from: " ~ appConfig.refreshTokenFilePath);
addLogEntry(" Error Reason:" ~ exception.msg);
authorised = false;
}
}
if (refreshToken.empty) {
// PROBLEM ... CODING TO DO ??????????
if (debugLogging) {addLogEntry("DEBUG: refreshToken is empty !!!!!!!!!!", ["debug"]);}
}
}
}
@ -398,94 +412,221 @@ class OneDriveApi {
// Authenticate this client against Microsoft OneDrive API
bool authorise() {
char[] response;
// What URL should be presented to the user to access
string url = authUrl ~ "?client_id=" ~ clientId ~ authScope ~ redirectUrl;
// Configure automated authentication if --auth-files authUrl:responseUrl is being used
string authFilesString = appConfig.getValueString("auth_files");
string authResponseString = appConfig.getValueString("auth_response");
if (!authResponseString.empty) {
// read the response from authResponseString
response = cast(char[]) authResponseString;
} else if (authFilesString != "") {
string[] authFiles = authFilesString.split(":");
string authUrl = authFiles[0];
string responseUrl = authFiles[1];
try {
auto authUrlFile = File(authUrl, "w");
authUrlFile.write(url);
authUrlFile.close();
} catch (FileException exception) {
// There was a file system error
// display the error message
displayFileSystemErrorMessage(exception.msg, getFunctionName!({}));
// Must force exit here, allow logging to be done
forceExit();
} catch (ErrnoException exception) {
// There was a file system error
// display the error message
displayFileSystemErrorMessage(exception.msg, getFunctionName!({}));
// Must force exit here, allow logging to be done
forceExit();
}
addLogEntry("Client requires authentication before proceeding. Waiting for --auth-files elements to be available.");
while (!exists(responseUrl)) {
Thread.sleep(dur!("msecs")(100));
}
// read response from provided from OneDrive
try {
response = cast(char[]) read(responseUrl);
} catch (OneDriveException exception) {
// exception generated
displayOneDriveErrorMessage(exception.msg, getFunctionName!({}));
return false;
}
// try to remove old files
try {
std.file.remove(authUrl);
std.file.remove(responseUrl);
} catch (FileException exception) {
addLogEntry("Cannot remove files " ~ authUrl ~ " " ~ responseUrl);
return false;
// Has the client been configured to use Intune SSO via Microsoft Identity Broker (microsoft-identity-broker) dbus session
if (appConfig.getValueBool("use_intune_sso")) {
// The client is configured to use Intune SSO via Microsoft Identity Broker dbus session
// Do we have a saved account file?
if (!exists(appConfig.intuneAccountDetailsFilePath)) {
// No file exists locally
auto intuneAuthResult = acquire_token_interactive(appConfig.getValueString("application_id"));
JSONValue intuneBrokerJSONData = intuneAuthResult.brokerTokenResponse;
// Is the response JSON data valid?
if ((intuneBrokerJSONData.type() == JSONType.object)) {
// Does the JSON data have the required authentication elements:
// - accessToken
// - account
// - expiresOn
if ((hasAccessTokenData(intuneBrokerJSONData)) && (hasAccountData(intuneBrokerJSONData)) && (hasExpiresOn(intuneBrokerJSONData))) {
// Details exist
processIntuneResponse(intuneBrokerJSONData);
// Return that we are authenticated
return true;
} else {
// no ... expected values not available
addLogEntry("Required JSON elements are not present in the Intune JSON response");
return false;
}
} else {
// Not a valid JSON response
addLogEntry("Invalid JSON Intune JSON response when attempting access authentication");
return false;
}
} else {
// The account information is available in a saved file. Read this file in and attempt a silent authentication
try {
appConfig.intuneAccountDetails = strip(readText(appConfig.intuneAccountDetailsFilePath));
// Is the 'intune_account' empty?
if (appConfig.intuneAccountDetails.empty) {
addLogEntry("The 'intune_account' file exists but is empty: " ~ appConfig.intuneAccountDetailsFilePath);
// No .. remove the file and perform the interactive authentication
safeRemove(appConfig.intuneAccountDetailsFilePath);
// Attempt interactive authentication
authorise();
return true;
} else {
// We have loaded some Intune Account details, try and use them
auto intuneAuthResult = acquire_token_silently(appConfig.intuneAccountDetails, appConfig.getValueString("application_id"));
JSONValue intuneBrokerJSONData = intuneAuthResult.brokerTokenResponse;
// Is the JSON data valid?
if ((intuneBrokerJSONData.type() == JSONType.object)) {
// Does the JSON data have the required authentication elements:
// - accessToken
// - account
// - expiresOn
if ((hasAccessTokenData(intuneBrokerJSONData)) && (hasAccountData(intuneBrokerJSONData)) && (hasExpiresOn(intuneBrokerJSONData))) {
// Details exist
processIntuneResponse(intuneBrokerJSONData);
// Return that we are authenticated
return true;
} else {
// no ... expected values not available
addLogEntry("Required JSON elements are not present in the Intune JSON response");
return false;
}
} else {
// No .. remove the file and perform the interactive authentication
safeRemove(appConfig.intuneAccountDetailsFilePath);
// Attempt interactive authentication
authorise();
return true;
}
}
} catch (FileException exception) {
return false;
} catch (std.utf.UTFException exception) {
// path contains characters which generate a UTF exception
addLogEntry("Cannot read 'intune_account' file on disk from: " ~ appConfig.intuneAccountDetailsFilePath);
addLogEntry(" Error Reason:" ~ exception.msg);
return false;
}
}
} else {
// Are we in a --dry-run scenario?
if (!appConfig.getValueBool("dry_run")) {
// No --dry-run is being used
addLogEntry("Authorise this application by visiting:\n", ["consoleOnly"]);
addLogEntry(url ~ "\n", ["consoleOnly"]);
addLogEntry("Enter the response uri from your browser: ", ["consoleOnlyNoNewLine"]);
readln(response);
appConfig.applicationAuthorizeResponseUri = true;
// Normal authentication method
char[] response;
// What URL should be presented to the user to access
string url = authUrl ~ "?client_id=" ~ clientId ~ authScope ~ redirectUrl;
// Configure automated authentication if --auth-files authUrl:responseUrl is being used
string authFilesString = appConfig.getValueString("auth_files");
string authResponseString = appConfig.getValueString("auth_response");
if (!authResponseString.empty) {
// read the response from authResponseString
response = cast(char[]) authResponseString;
} else if (authFilesString != "") {
string[] authFiles = authFilesString.split(":");
string authUrl = authFiles[0];
string responseUrl = authFiles[1];
try {
auto authUrlFile = File(authUrl, "w");
authUrlFile.write(url);
authUrlFile.close();
} catch (FileException exception) {
// There was a file system error
// display the error message
displayFileSystemErrorMessage(exception.msg, getFunctionName!({}));
// Must force exit here, allow logging to be done
forceExit();
} catch (ErrnoException exception) {
// There was a file system error
// display the error message
displayFileSystemErrorMessage(exception.msg, getFunctionName!({}));
// Must force exit here, allow logging to be done
forceExit();
}
addLogEntry("Client requires authentication before proceeding. Waiting for --auth-files elements to be available.");
while (!exists(responseUrl)) {
Thread.sleep(dur!("msecs")(100));
}
// read response from provided from OneDrive
try {
response = cast(char[]) read(responseUrl);
} catch (OneDriveException exception) {
// exception generated
displayOneDriveErrorMessage(exception.msg, getFunctionName!({}));
return false;
}
// try to remove old files
try {
std.file.remove(authUrl);
std.file.remove(responseUrl);
} catch (FileException exception) {
addLogEntry("Cannot remove files " ~ authUrl ~ " " ~ responseUrl);
return false;
}
} else {
// The application cannot be authorised when using --dry-run as we have to write out the authentication data, which negates the whole 'dry-run' process
addLogEntry();
addLogEntry("The application requires authorisation, which involves saving authentication data on your system. Application authorisation cannot be completed when using the '--dry-run' option.");
addLogEntry();
addLogEntry("To authorise the application please use your original command without '--dry-run'.");
addLogEntry();
addLogEntry("To exclusively authorise the application without performing any additional actions, do not add '--sync' or '--monitor' to your command line.");
addLogEntry();
forceExit();
// Are we in a --dry-run scenario?
if (!appConfig.getValueBool("dry_run")) {
// No --dry-run is being used
addLogEntry("Authorise this application by visiting:\n", ["consoleOnly"]);
addLogEntry(url ~ "\n", ["consoleOnly"]);
addLogEntry("Enter the response uri from your browser: ", ["consoleOnlyNoNewLine"]);
readln(response);
appConfig.applicationAuthorizeResponseUri = true;
} else {
// The application cannot be authorised when using --dry-run as we have to write out the authentication data, which negates the whole 'dry-run' process
addLogEntry();
addLogEntry("The application requires authorisation, which involves saving authentication data on your system. Application authorisation cannot be completed when using the '--dry-run' option.");
addLogEntry();
addLogEntry("To authorise the application please use your original command without '--dry-run'.");
addLogEntry();
addLogEntry("To exclusively authorise the application without performing any additional actions, do not add '--sync' or '--monitor' to your command line.");
addLogEntry();
forceExit();
}
}
// match the authorisation code
auto c = matchFirst(response, r"(?:[\?&]code=)([\w\d-.]+)");
if (c.empty) {
addLogEntry("An empty or invalid response uri was entered");
return false;
}
c.popFront(); // skip the whole match
redeemToken(c.front);
return true;
}
}
// Process Intune JSON response data
void processIntuneResponse(JSONValue intuneBrokerJSONData) {
// Use the provided JSON data and configure elements, save JSON data to disk for reuse
long expiresOnMs = intuneBrokerJSONData["expiresOn"].integer();
// Convert to SysTime
SysTime expiryTime = SysTime.fromUnixTime(expiresOnMs / 1000);
// Store in appConfig (to match standard flow)
appConfig.accessTokenExpiration = expiryTime;
addLogEntry("Intune access token expires at: " ~ to!string(appConfig.accessTokenExpiration));
// Configure the 'accessToken' based on Intune response
appConfig.accessToken = "bearer " ~ strip(intuneBrokerJSONData["accessToken"].str);
// Do we print the current access token
debugOutputAccessToken();
// In order to support silent renewal of the access token, when the access token expires, we must store the Intune account data
appConfig.intuneAccountDetails = to!string(intuneBrokerJSONData["account"]);
// try and update the 'intune_account' file on disk for reuse later
try {
if (debugLogging) {addLogEntry("Updating 'intune_account' on disk", ["debug"]);}
std.file.write(appConfig.intuneAccountDetailsFilePath, appConfig.intuneAccountDetails);
if (debugLogging) {addLogEntry("Setting file permissions for: " ~ appConfig.intuneAccountDetailsFilePath, ["debug"]);}
appConfig.intuneAccountDetailsFilePath.setAttributes(appConfig.returnSecureFilePermission());
} catch (FileException exception) {
// display the error message
displayFileSystemErrorMessage(exception.msg, getFunctionName!({}));
}
}
// Do we print the current access token
void debugOutputAccessToken() {
if (appConfig.verbosityCount > 1) {
if (appConfig.getValueBool("debug_https")) {
if (appConfig.getValueBool("print_token")) {
// This needs to be highly restricted in output ....
if (debugLogging) {addLogEntry("CAUTION - KEEP THIS SAFE: Current access token: " ~ to!string(appConfig.accessToken), ["debug"]);}
}
}
}
// match the authorization code
auto c = matchFirst(response, r"(?:[\?&]code=)([\w\d-.]+)");
if (c.empty) {
addLogEntry("An empty or invalid response uri was entered");
return false;
}
c.popFront(); // skip the whole match
redeemToken(c.front);
return true;
}
// https://docs.microsoft.com/en-us/onedrive/developer/rest-api/api/drive_get
@ -870,6 +1011,8 @@ class OneDriveApi {
private void acquireToken(char[] postData) {
JSONValue response;
// Debug this input
if (debugLogging) {addLogEntry("Manual Auth postData = " ~ to!string(postData), ["debug"]);}
try {
response = post(tokenUrl, postData, null, true, "application/x-www-form-urlencoded");
@ -893,6 +1036,9 @@ class OneDriveApi {
}
if (response.type() == JSONType.object) {
// Debug this response
if (debugLogging) {addLogEntry("Manual Auth Response JSON = " ~ to!string(response), ["debug"]);}
// Has the client been configured to use read_only_auth_scope
if (appConfig.getValueBool("read_only_auth_scope")) {
// read_only_auth_scope has been configured
@ -922,17 +1068,15 @@ class OneDriveApi {
appConfig.accessToken = "bearer " ~ strip(response["access_token"].str);
// Do we print the current access token
if (appConfig.verbosityCount > 1) {
if (appConfig.getValueBool("debug_https")) {
if (appConfig.getValueBool("print_token")) {
// This needs to be highly restricted in output ....
if (debugLogging) {addLogEntry("CAUTION - KEEP THIS SAFE: Current access token: " ~ to!string(appConfig.accessToken), ["debug"]);}
}
}
}
debugOutputAccessToken();
// Obtain the 'refresh_token' and its expiry
refreshToken = strip(response["refresh_token"].str);
appConfig.accessTokenExpiration = Clock.currTime() + dur!"seconds"(response["expires_in"].integer());
// Debug this response
if (debugLogging) {addLogEntry("appConfig.accessTokenExpiration = " ~ to!string(appConfig.accessTokenExpiration), ["debug"]);}
if (!dryRun) {
// Update the refreshToken in appConfig so that we can reuse it
if (appConfig.refreshToken.empty) {
@ -948,12 +1092,12 @@ class OneDriveApi {
}
}
// try and update the refresh_token file on disk
// try and update the 'refresh_token' file on disk
try {
if (debugLogging) {addLogEntry("Updating refreshToken on disk", ["debug"]);}
if (debugLogging) {addLogEntry("Updating 'refresh_token' on disk", ["debug"]);}
std.file.write(appConfig.refreshTokenFilePath, refreshToken);
if (debugLogging) {addLogEntry("Setting file permissions for: " ~ appConfig.refreshTokenFilePath, ["debug"]);}
appConfig.refreshTokenFilePath.setAttributes(appConfig.returnRequiredFilePermissions());
appConfig.refreshTokenFilePath.setAttributes(appConfig.returnSecureFilePermission());
} catch (FileException exception) {
// display the error message
displayFileSystemErrorMessage(exception.msg, getFunctionName!({}));
@ -976,20 +1120,46 @@ class OneDriveApi {
}
}
private void newToken() {
private void generateNewAccessToken() {
if (debugLogging) {addLogEntry("Need to generate a new access token for Microsoft OneDrive", ["debug"]);}
auto postData = appender!(string)();
postData ~= "client_id=" ~ clientId;
postData ~= "&redirect_uri=" ~ redirectUrl;
postData ~= "&refresh_token=" ~ to!string(refreshToken);
postData ~= "&grant_type=refresh_token";
acquireToken(postData.data.dup);
// Has the client been configured to use Intune SSO via Microsoft Identity Broker (microsoft-identity-broker) dbus session
if (appConfig.getValueBool("use_intune_sso")) {
// The client is configured to use Intune SSO via Microsoft Identity Broker dbus session
auto intuneAuthResult = acquire_token_silently(appConfig.intuneAccountDetails, appConfig.getValueString("application_id"));
JSONValue intuneBrokerJSONData = intuneAuthResult.brokerTokenResponse;
// Is the JSON data valid?
if ((intuneBrokerJSONData.type() == JSONType.object)) {
// Does the JSON data have the required renewal elements:
// - accessToken
// - account
// - expiresOn
if ((hasAccessTokenData(intuneBrokerJSONData)) && (hasAccountData(intuneBrokerJSONData)) && (hasExpiresOn(intuneBrokerJSONData))) {
// Details exist
processIntuneResponse(intuneBrokerJSONData);
} else {
// no ... expected values not available
addLogEntry("Required Intune JSON elements are not present in the Intune JSON response");
}
} else {
// Not a valid JSON response
addLogEntry("Invalid Intune JSON response when attempting access token renewal");
}
} else {
// Normal authentication method
auto postData = appender!(string)();
postData ~= "client_id=" ~ clientId;
postData ~= "&redirect_uri=" ~ redirectUrl;
postData ~= "&refresh_token=" ~ to!string(refreshToken);
postData ~= "&grant_type=refresh_token";
acquireToken(postData.data.dup);
}
}
// Check if the existing access token has expired, if it has, generate a new one
private void checkAccessTokenExpired() {
if (Clock.currTime() >= appConfig.accessTokenExpiration) {
if (debugLogging) {addLogEntry("Microsoft OneDrive Access Token has expired. Must generate a new Microsoft OneDrive Access Token", ["debug"]);}
newToken();
generateNewAccessToken();
} else {
if (debugLogging) {addLogEntry("Existing Microsoft OneDrive Access Token Expires: " ~ to!string(appConfig.accessTokenExpiration), ["debug"]);}
}

View file

@ -1352,6 +1352,21 @@ bool hasLastModifiedByUserDisplayName(const ref JSONValue item) {
}
}
// Check Intune JSON response for 'accessToken'
bool hasAccessTokenData(const ref JSONValue item) {
return ("accessToken" in item) != null;
}
// Check Intune JSON response for 'account'
bool hasAccountData(const ref JSONValue item) {
return ("account" in item) != null;
}
// Check Intune JSON response for 'expiresOn'
bool hasExpiresOn(const ref JSONValue item) {
return ("expiresOn" in item) != null;
}
// Convert bytes to GB
string byteToGibiByte(ulong bytes) {
if (bytes == 0) {