Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WIP: Sync ODB shared folders from external organisation #969

Closed
wants to merge 36 commits into from
Closed
Show file tree
Hide file tree
Changes from 21 commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
7a51729
WIP: Sync ODB shared folders from external organisation
abraunegg Jun 27, 2020
7538ea1
Merge branch 'master' into fix-issue-966
abraunegg Jun 28, 2020
0558379
Update --list-shared-folders to support external shared folders
abraunegg Jun 30, 2020
d503b7e
Merge branch 'master' into fix-issue-966
abraunegg Jun 30, 2020
25aef4c
Merge branch 'master' into fix-issue-966
abraunegg Jun 30, 2020
6b0d42f
Merge branch 'fix-issue-966' of https://github.com/abraunegg/onedrive…
abraunegg Jun 30, 2020
96e02a3
Update code
abraunegg Jul 1, 2020
f853e86
Merge branch 'master' into fix-issue-966
abraunegg Jul 2, 2020
75de649
Merge branch 'master' into fix-issue-966
abraunegg Jul 7, 2020
b84f302
Merge branch 'master' into fix-issue-966
abraunegg Jul 21, 2020
5a8c129
Merge branch 'master' into fix-issue-966
abraunegg Jul 22, 2020
c208dac
Merge branch 'master' into fix-issue-966
abraunegg Jul 29, 2020
2abd427
Merge branch 'master' into fix-issue-966
abraunegg Aug 10, 2020
e9a3e27
Merge branch 'master' into fix-issue-966
abraunegg Aug 14, 2020
7e153e0
Merge 'master' into PR and resolve conflicts
abraunegg Sep 18, 2020
d7875f7
Merge branch 'fix-issue-966' of https://github.com/abraunegg/onedrive…
abraunegg Sep 18, 2020
5605a85
Merge branch 'master' into fix-issue-966
abraunegg Sep 21, 2020
64f894e
Merge branch 'master' into fix-issue-966
abraunegg Oct 4, 2020
4c99d53
Merge branch 'master' into fix-issue-966
abraunegg Oct 8, 2020
50137b5
Merge 'master' to PR
abraunegg Dec 1, 2020
14b83bb
Update BusinessSharedFolders.md
abraunegg Dec 1, 2020
e764151
update pr from master
abraunegg Dec 11, 2020
2a93b4e
Merge branch 'master' into fix-issue-966
abraunegg Dec 11, 2020
0e886ac
Handle exception error when query for tenant id
abraunegg Dec 11, 2020
bd124bf
Merge branch 'master' into fix-issue-966
abraunegg Dec 26, 2020
a8bec4b
Merge branch 'master' into fix-issue-966
abraunegg Mar 5, 2021
c4807fd
Merge branch 'master' into fix-issue-966
abraunegg Apr 6, 2021
d9abb8f
Merge branch 'master' into fix-issue-966
abraunegg May 28, 2021
5f31f5e
Update PR from 'Master'
abraunegg Jul 13, 2021
4df9b00
Update PR from Master
abraunegg May 23, 2022
cf05b19
Merge branch 'master' into fix-issue-966
abraunegg May 31, 2022
8850f7c
Merge branch 'master' into fix-issue-966
abraunegg Jun 3, 2022
47c3dcd
Merge branch 'master' into fix-issue-966
abraunegg Jun 15, 2022
0b147e6
Merge branch 'master' into fix-issue-966
abraunegg Jun 29, 2022
c41c056
Merge branch 'master' into fix-issue-966
abraunegg Jul 22, 2022
661e2cb
Merge branch 'master' into fix-issue-966
abraunegg Aug 2, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 0 additions & 9 deletions docs/BusinessSharedFolders.md
Original file line number Diff line number Diff line change
Expand Up @@ -177,12 +177,3 @@ sync_business_shared_folders = "true"
```text
sync_business_shared_folders = "false"
```

## Known Issues
Shared folders, shared with you from people outside of your 'organisation' are unable to be synced. This is due to the Microsoft Graph API not presenting these folders.

Shared folders that match this scenario, when you view 'Shared' via OneDrive online, will have a 'world' symbol as per below:

![shared_with_me](./images/shared_with_me.JPG)

This issue is being tracked by: [#966](https://github.com/abraunegg/onedrive/issues/966)
156 changes: 131 additions & 25 deletions src/onedrive.d
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,9 @@ private {
// What is 'shared with me' Query
string sharedWithMe = globalGraphEndpoint ~ "/v1.0/me/drive/sharedWithMe";

// What are my 'tenant' details
string sharepointTenantId = globalGraphEndpoint ~ "/v1.0/organization";

// Item Queries
string itemByIdUrl = globalGraphEndpoint ~ "/v1.0/me/drive/items/";
string itemByPathUrl = globalGraphEndpoint ~ "/v1.0/me/drive/root:/";
Expand Down Expand Up @@ -105,8 +108,16 @@ final class OneDriveApi
private SysTime accessTokenExpiration;
private HTTP http;

// if true, every new access token is printed
// If true, every new access token is printed
bool printAccessToken;

// OneDrive Business External Shared Folder tenant handling
bool externalTenant = false;
string externalTenantID = "";
string externalTokenUrl = "";
string externalRefreshToken = "";
string externalAccessToken = "";
SysTime externalAccessTokenExpiration;

this(Config cfg)
{
Expand Down Expand Up @@ -196,6 +207,7 @@ final class OneDriveApi
siteDriveUrl = usl4GraphEndpoint ~ "/v1.0/sites/";
// Shared With Me
sharedWithMe = usl4GraphEndpoint ~ "/v1.0/me/drive/sharedWithMe";
sharepointTenantId = usl4GraphEndpoint ~ "/v1.0/organization";
break;
case "USL5":
log.log("Configuring Azure AD for US Government Endpoints (DOD)");
Expand All @@ -222,6 +234,7 @@ final class OneDriveApi
siteDriveUrl = usl5GraphEndpoint ~ "/v1.0/sites/";
// Shared With Me
sharedWithMe = usl5GraphEndpoint ~ "/v1.0/me/drive/sharedWithMe";
sharepointTenantId = usl5GraphEndpoint ~ "/v1.0/organization";
break;
case "DE":
log.log("Configuring Azure AD Germany");
Expand All @@ -248,6 +261,7 @@ final class OneDriveApi
siteDriveUrl = deGraphEndpoint ~ "/v1.0/sites/";
// Shared With Me
sharedWithMe = deGraphEndpoint ~ "/v1.0/me/drive/sharedWithMe";
sharepointTenantId = deGraphEndpoint ~ "/v1.0/organization";
break;
case "CN":
log.log("Configuring AD China operated by 21Vianet");
Expand All @@ -274,6 +288,7 @@ final class OneDriveApi
siteDriveUrl = cnGraphEndpoint ~ "/v1.0/sites/";
// Shared With Me
sharedWithMe = cnGraphEndpoint ~ "/v1.0/me/drive/sharedWithMe";
sharepointTenantId = cnGraphEndpoint ~ "/v1.0/organization";
break;
// Default - all other entries
default:
Expand Down Expand Up @@ -379,7 +394,7 @@ final class OneDriveApi
{
import std.stdio, std.regex;
char[] response;
string url = authUrl ~ "?client_id=" ~ clientId ~ "&scope=Files.ReadWrite%20Files.ReadWrite.all%20Sites.Read.All%20Sites.ReadWrite.All%20offline_access&response_type=code&redirect_uri=" ~ redirectUrl;
string url = authUrl ~ "?client_id=" ~ clientId ~ "&scope=User.Read%20Files.ReadWrite%20Files.ReadWrite.all%20Sites.Read.All%20Sites.ReadWrite.All%20offline_access&response_type=code&redirect_uri=" ~ redirectUrl;
string authFilesString = cfg.getValueString("auth_files");
if (authFilesString == "") {
log.log("Authorize this app visiting:\n");
Expand Down Expand Up @@ -438,6 +453,50 @@ final class OneDriveApi
.retryAfterValue = 0;
}

void setExternalTenant(string externalTenantValue)
{
// Flag that we are using an external tenant
externalTenant = true;
externalTenantID = externalTenantValue;

// Configure externalTokenUrl for this external tenant
string azureConfigValue = cfg.getValueString("azure_ad_endpoint");
switch(azureConfigValue) {
case "":
externalTokenUrl = globalAuthEndpoint ~ "/" ~ externalTenantID ~ "/oauth2/v2.0/token";
break;
case "USL4":
externalTokenUrl = usl4AuthEndpoint ~ "/" ~ externalTenantID ~ "/oauth2/v2.0/token";
break;
case "USL5":
externalTokenUrl = usl5AuthEndpoint ~ "/" ~ externalTenantID ~ "/oauth2/v2.0/token";
break;
case "DE":
externalTokenUrl = deAuthEndpoint ~ "/" ~ externalTenantID ~ "/oauth2/v2.0/token";
break;
case "CN":
externalTokenUrl = cnAuthEndpoint ~ "/" ~ externalTenantID ~ "/oauth2/v2.0/token";
tokenUrl = cnAuthEndpoint ~ "/common/oauth2/v2.0/token";
break;
// Default - all other entries
default:
externalTokenUrl = globalAuthEndpoint ~ "/" ~ externalTenantID ~ "/oauth2/v2.0/token";
}

// Get new token and configure this for use
newToken();
}

void clearExternalTenant()
{
// Clear any external tenant that was set
externalTenant = false;
externalTenantID = "";
externalTokenUrl = "";
externalRefreshToken = "";
externalAccessToken = "";
}

// https://docs.microsoft.com/en-us/onedrive/developer/rest-api/api/drive_get
JSONValue getDefaultDrive()
{
Expand Down Expand Up @@ -465,11 +524,23 @@ final class OneDriveApi
return get(url);
}

// https://docs.microsoft.com/en-us/onedrive/developer/rest-api/api/site_get
JSONValue getTenantID()
{
checkAccessTokenExpired();
const(char)[] url;
url = sharepointTenantId;
return get(url);
}

// https://docs.microsoft.com/en-us/graph/api/drive-sharedwithme
JSONValue getSharedWithMe()
{
checkAccessTokenExpired();
return get(sharedWithMe);
const(char)[] url;
url = sharedWithMe;
url ~= "?allowexternal=true";
return get(url);
}

// https://docs.microsoft.com/en-us/onedrive/developer/rest-api/api/drive_get
Expand Down Expand Up @@ -651,7 +722,7 @@ final class OneDriveApi
return get(url);
}

// Return the requested details of the specified id
// Return the requested details of the specified item id
// https://docs.microsoft.com/en-us/onedrive/developer/rest-api/api/driveitem_get
JSONValue getFileDetails(const(char)[] driveId, const(char)[] id)
{
Expand Down Expand Up @@ -775,38 +846,60 @@ final class OneDriveApi
private void acquireToken(const(char)[] postData)
{
JSONValue response;

try {
response = post(tokenUrl, postData);
if (!externalTenant) {
// use normal token url
response = post(tokenUrl, postData);
} else {
// use external tenant token url
response = post(externalTokenUrl, postData);
}
} catch (OneDriveException e) {
// an error was generated
displayOneDriveErrorMessage(e.msg);
}

if (response.type() == JSONType.object) {
if ("access_token" in response){
accessToken = "bearer " ~ response["access_token"].str();
refreshToken = response["refresh_token"].str();
accessTokenExpiration = Clock.currTime() + dur!"seconds"(response["expires_in"].integer());
if (!.dryRun) {
try {
// try and update the refresh_token file
std.file.write(cfg.refreshTokenFilePath, refreshToken);
log.vdebug("Setting file permissions for: ", cfg.refreshTokenFilePath);
cfg.refreshTokenFilePath.setAttributes(cfg.returnRequiredFilePermisions());
} catch (FileException e) {
// display the error message
displayFileSystemErrorMessage(e.msg);
// what sort of tenant token are we processing?
if (!externalTenant) {
// Normal tenant / token processing
if ("access_token" in response){
accessToken = "bearer " ~ response["access_token"].str();
refreshToken = response["refresh_token"].str();
accessTokenExpiration = Clock.currTime() + dur!"seconds"(response["expires_in"].integer());
if (!.dryRun) {
try {
// try and update the refresh_token file
log.vdebug("Updating refresh_token file with new token from OneDrive");
std.file.write(cfg.refreshTokenFilePath, refreshToken);
log.vdebug("Setting file permissions for: ", cfg.refreshTokenFilePath);
cfg.refreshTokenFilePath.setAttributes(cfg.returnRequiredFilePermisions());
} catch (FileException e) {
// display the error message
displayFileSystemErrorMessage(e.msg);
}
}
if (printAccessToken) writeln("New access token: ", accessToken);
} else {
log.error("\nInvalid authentication response from OneDrive. Please check the response uri\n");
// re-authorize
authorize();
}
if (printAccessToken) writeln("New access token: ", accessToken);
} else {
log.error("\nInvalid authentication response from OneDrive. Please check the response uri\n");
// re-authorize
authorize();
// External tenant token handling
if ("access_token" in response){
log.vdebug("Updating access token with new token received from External OneDrive 3rd Party");
externalAccessToken = "bearer " ~ response["access_token"].str();
log.vdebug("Updating refresh_token with new token received from External OneDrive 3rd Party");
externalRefreshToken = response["refresh_token"].str();
externalAccessTokenExpiration = Clock.currTime() + dur!"seconds"(response["expires_in"].integer());
if (printAccessToken) writeln("New external access token: ", externalAccessToken);
} else {
log.error("\nInvalid authentication response from OneDrive. Please check the response uri\n");
}
}
} else {
log.vdebug("Invalid JSON response from OneDrive unable to initialize application");
log.vdebug("Invalid JSON response from OneDrive unable to aquire token to initialize application");
}
}

Expand All @@ -828,7 +921,20 @@ final class OneDriveApi

private void addAccessTokenHeader()
{
http.addRequestHeader("Authorization", accessToken);
// Which access token header must we add?
if (!externalTenant) {
// Add the default client accessToken
http.addRequestHeader("Authorization", accessToken);
} else {
// Add the external tenant accessToken
if (externalAccessToken.empty) {
// if this is still empty, we are probably in the process of getting an updated external access token
// use our original one for the moment until this is set
http.addRequestHeader("Authorization", accessToken);
} else {
http.addRequestHeader("Authorization", externalAccessToken);
}
}
}

private JSONValue get(const(char)[] url, bool skipToken = false)
Expand Down
59 changes: 55 additions & 4 deletions src/sync.d
Original file line number Diff line number Diff line change
Expand Up @@ -596,11 +596,24 @@ final class SyncEngine

// Check OneDrive Business Shared Folders, if configured to do so
if (syncBusinessFolders){
// query OneDrive Business Shared Folders shared with me
// Get My Tenent Details
string myTenantID;
JSONValue tenantDetailsResponse = onedrive.getTenantID();
if (tenantDetailsResponse.type() == JSONType.object) {
foreach (searchResult; tenantDetailsResponse["value"].array) {
myTenantID = searchResult["id"].str;
}
} else {
// Log that an invalid JSON object was returned
log.error("ERROR: onedrive.getTenantID call returned an invalid JSON Object");
}

// Query OneDrive Business Shared Folders shared with me
log.vlog("Attempting to sync OneDrive Business Shared Folders");
JSONValue graphQuery = onedrive.getSharedWithMe();
if (graphQuery.type() == JSONType.object) {
string sharedFolderName;
bool isExternalTenant = false;
foreach (searchResult; graphQuery["value"].array) {
// Configure additional logging items for this array element
string sharedByName;
Expand All @@ -626,16 +639,25 @@ final class SyncEngine
bool itemInDatabase = false;
bool itemLocalDirExists = false;
bool itemPathIsLocal = false;
isExternalTenant = false;

// "what if" there are 2 or more folders shared with me have the "same" name?
// The folder name will be the same, but driveId will be different
// This will then cause these 'shared folders' to cross populate data, which may not be desirable
log.vdebug("Shared Folder Name: ", sharedFolderName);
log.vdebug("Parent Drive Id: ", searchResult["remoteItem"]["parentReference"]["driveId"].str);
log.vdebug("Shared Item Id: ", searchResult["remoteItem"]["id"].str);
Item databaseItem;

// for each driveid in the existing driveIDsArray
// Is this OneDrive Shared Folder on an external tenant?
if (searchResult["remoteItem"]["sharepointIds"]["tenantId"].str != myTenantID) {
isExternalTenant = true;
log.vdebug("This shared folder is shared from an external organisation as the tenant is different");
// Have to configure the access to this tenant, which requires separate tokenUrl for that tenant
onedrive.setExternalTenant(searchResult["remoteItem"]["sharepointIds"]["tenantId"].str);
}

// for each driveid in the existing driveIDsArray
Item databaseItem;
foreach (searchDriveId; driveIDsArray) {
log.vdebug("searching database for: ", searchDriveId, " ", sharedFolderName);
if (itemdb.selectByPath(sharedFolderName, searchDriveId, databaseItem)) {
Expand Down Expand Up @@ -709,7 +731,7 @@ final class SyncEngine
}
}
}
}
}
} else {
// not a folder, is this a file?
if (isItemFile(searchResult)) {
Expand All @@ -731,6 +753,12 @@ final class SyncEngine
log.log("WARNING: Not syncing this OneDrive Business Shared item: ", searchResult["name"].str);
}
}

// Was this shared folder on an external tenant?
if (isExternalTenant) {
// clear external tenant
onedrive.clearExternalTenant();
}
}
} else {
// Log that an invalid JSON object was returned
Expand Down Expand Up @@ -6107,6 +6135,19 @@ final class SyncEngine
{
// List OneDrive Business Shared Folders
log.log("\nListing available OneDrive Business Shared Folders:");

// Get My Tenent Details
string myTenantID;
JSONValue tenantDetailsResponse = onedrive.getTenantID();
if (tenantDetailsResponse.type() == JSONType.object) {
foreach (searchResult; tenantDetailsResponse["value"].array) {
myTenantID = searchResult["id"].str;
}
} else {
// Log that an invalid JSON object was returned
log.error("ERROR: onedrive.getTenantID call returned an invalid JSON Object");
}

// Query the GET /me/drive/sharedWithMe API
JSONValue graphQuery = onedrive.getSharedWithMe();
if (graphQuery.type() == JSONType.object) {
Expand Down Expand Up @@ -6140,6 +6181,7 @@ final class SyncEngine
}
// Output query result
log.log("---------------------------------------");
// Default output
log.log("Shared Folder: ", sharedFolderName);
if ((sharedByName != "") && (sharedByEmail != "")) {
log.log("Shared By: ", sharedByName, " (", sharedByEmail, ")");
Expand All @@ -6148,11 +6190,20 @@ final class SyncEngine
log.log("Shared By: ", sharedByName);
}
}
// Tenant details
if (searchResult["remoteItem"]["sharepointIds"]["tenantId"].str == myTenantID) {
log.log("External Organisation: no");
} else {
log.log("External Organisation: yes");
}

// Extra verbose output
log.vlog("Item Id: ", searchResult["remoteItem"]["id"].str);
log.vlog("Parent Drive Id: ", searchResult["remoteItem"]["parentReference"]["driveId"].str);
if ("id" in searchResult["remoteItem"]["parentReference"]) {
log.vlog("Parent Item Id: ", searchResult["remoteItem"]["parentReference"]["id"].str);
}
log.vlog("Tenant ID: ", searchResult["remoteItem"]["sharepointIds"]["tenantId"].str);
}
}
}
Expand Down