Skip to content

Commit

Permalink
Bring back OneDrive
Browse files Browse the repository at this point in the history
  • Loading branch information
themylogin committed Jan 10, 2025
1 parent c89c2d6 commit 946fdab
Show file tree
Hide file tree
Showing 8 changed files with 167 additions and 16 deletions.
18 changes: 18 additions & 0 deletions docs/source/middleware/plugins/cloudsync.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
`cloudsync` plugin: Cloud Sync
==============================

.. contents:: Table of Contents
:depth: 3

OAuth
-----

Some cloud providers offer the option to configure access using the standard OAuth flow through the user's browser.
The process works as follows:

* The UI opens a pop-up window with the URL: `https://truenas.com/oauth/<provider>?origin=<NAS IP>`
* The `truenas.com` web server proxies this request to an installation of the
`oauth-portal <https://github.com/ixsystems/oauth-portal>`_.
* The OAuth portal forwards the request to the corresponding OAuth provider, retrieves the tokens, and returns them to
the UI using `window.opener.postMessage`.
* The UI receives the tokens and populates the corresponding Cloud Credentials configuration form.
1 change: 1 addition & 0 deletions docs/source/middleware/plugins/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,5 @@ Middleware Plugins
:caption: Contents:

alert.rst
cloudsync.rst
migration.rst
2 changes: 1 addition & 1 deletion docs/source/simulating/index.rst
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
Simulating real-world conditions
Simulating Real-World Conditions
================================

.. toctree::
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,7 @@


def upgrade():
conn = op.get_bind()
conn.execute("DELETE FROM tasks_cloudsync WHERE credential_id in (SELECT id FROM system_cloudcredentials WHERE "
"provider = 'ONEDRIVE')")
conn.execute("DELETE FROM system_cloudcredentials WHERE provider = 'ONEDRIVE'")
pass


def downgrade():
Expand Down
10 changes: 10 additions & 0 deletions src/middlewared/middlewared/api/v25_04_0/cloud_sync_providers.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,15 @@ class MegaCredentialsModel(BaseModel):
pass_: Secret[NonEmptyString] = Field(alias="pass")


class OneDriveCredentialsModel(BaseModel):
type: Literal["ONEDRIVE"]
client_id: Secret[str] = ""
client_secret: Secret[str] = ""
token: Secret[LongNonEmptyString]
drive_type: Secret[Literal["PERSONAL", "BUSINESS", "DOCUMENT_LIBRARY"]]
drive_id: Secret[str]


class PCloudCredentialsModel(BaseModel):
type: Literal["PCLOUD"]
client_id: Secret[str] = ""
Expand Down Expand Up @@ -169,6 +178,7 @@ class YandexCredentialsModel(BaseModel):
HTTPCredentialsModel,
HubicCredentialsModel,
MegaCredentialsModel,
OneDriveCredentialsModel,
PCloudCredentialsModel,
S3CredentialsModel,
SFTPCredentialsModel,
Expand Down
17 changes: 6 additions & 11 deletions src/middlewared/middlewared/plugins/mail_/outlook.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import requests

from middlewared.service import CallError, private, Service
from middlewared.utils.microsoft import get_microsoft_access_token


@dataclass
Expand All @@ -29,18 +30,12 @@ def outlook_xoauth2(self, server: SMTP, config: dict):
self.logger.warning("Outlook XOAUTH2 failed: %r %r. Refreshing access token", code, response)

self.logger.debug("Requesting Outlook access token")
r = requests.post(
"https://login.microsoftonline.com/common/oauth2/v2.0/token",
data={
"grant_type": "refresh_token",
"client_id": config["oauth"]["client_id"],
"client_secret": config["oauth"]["client_secret"],
"refresh_token": config["oauth"]["refresh_token"],
"scope": "https://outlook.office.com/SMTP.Send openid offline_access",
}
response = get_microsoft_access_token(
config["oauth"]["client_id"],
config["oauth"]["client_secret"],
config["oauth"]["refresh_token"],
"https://outlook.office.com/SMTP.Send openid offline_access",
)
r.raise_for_status()
response = r.json()

token = response["access_token"]
self._set_outlook_token(config["fromemail"], config["oauth"]["refresh_token"], token, response["expires_in"])
Expand Down
113 changes: 113 additions & 0 deletions src/middlewared/middlewared/rclone/remote/onedrive.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
import json

import requests

from middlewared.rclone.base import BaseRcloneRemote
from middlewared.schema import Dict, Str
from middlewared.service import accepts
from middlewared.utils.microsoft import get_microsoft_access_token

DRIVES_TYPES = {
"PERSONAL": "personal",
"BUSINESS": "business",
"DOCUMENT_LIBRARY": "documentLibrary",
}
DRIVES_TYPES_INV = {v: k for k, v in DRIVES_TYPES.items()}


class OneDriveRcloneRemote(BaseRcloneRemote):
name = "ONEDRIVE"
title = "Microsoft OneDrive"

rclone_type = "onedrive"

credentials_oauth = True
refresh_credentials = ["token"]

extra_methods = ["list_drives"]

async def get_task_extra(self, task):
return {
"drive_type": DRIVES_TYPES.get(task["credentials"]["attributes"]["drive_type"], ""),
# Subject to change as Microsoft changes rate limits; please watch `forum.rclone.org`
"checkers": "1",
"tpslimit": "10",
}

@accepts(Dict(
"onedrive_list_drives",
Str("client_id", default=""),
Str("client_secret", default=""),
Str("token", required=True, max_length=None),
))
def list_drives(self, credentials):
"""
Lists all available drives and their types for given Microsoft OneDrive credentials.
.. examples(websocket)::
:::javascript
{
"id": "6841f242-840a-11e6-a437-00e04d680384",
"msg": "method",
"method": "cloudsync.onedrive_list_drives",
"params": [{
"client_id": "...",
"client_secret": "",
"token": "{...}",
}]
}
Returns
[{"drive_type": "PERSONAL", "drive_id": "6bb903a25ad65e46"}]
"""
self.middleware.call_sync("network.general.will_perform_activity", "cloud_sync")

if not credentials["client_id"]:
credentials["client_id"] = "b15665d9-eda6-4092-8539-0eec376afd59"
if not credentials["client_secret"]:
credentials["client_secret"] = "qtyfaBBYA403=unZUP40~_#"

token = json.loads(credentials["token"])

r = requests.get(
"https://graph.microsoft.com/v1.0/me/drives",
headers={"Authorization": f"Bearer {token['access_token']}"},
timeout=10,
)
if r.status_code == 401:
token = get_microsoft_access_token(
credentials["client_id"],
credentials["client_secret"],
token["refresh_token"],
"Files.Read Files.ReadWrite Files.Read.All Files.ReadWrite.All Sites.Read.All offline_access",
)
r = requests.get(
"https://graph.microsoft.com/v1.0/me/drives",
headers={"Authorization": f"Bearer {token['access_token']}"},
timeout=10,
)
r.raise_for_status()

def process_drive(drive):
return {
"drive_type": DRIVES_TYPES_INV.get(drive["driveType"], ""),
"drive_id": drive["id"],
}

result = []
for drive in r.json()["value"]:
result.append(process_drive(drive))
# Also call /me/drive as sometimes /me/drives doesn't return it
# see https://github.com/rclone/rclone/issues/4068
r = requests.get(
"https://graph.microsoft.com/v1.0/me/drive",
headers={"Authorization": f"Bearer {token['access_token']}"},
timeout=10,
)
r.raise_for_status()
me_drive = process_drive(r.json())
if me_drive not in result:
result.insert(0, me_drive)
return result
17 changes: 17 additions & 0 deletions src/middlewared/middlewared/utils/microsoft.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import requests


def get_microsoft_access_token(client_id: str, client_secret: str, refresh_token: str, scope: str) -> dict:
r = requests.post(
"https://login.microsoftonline.com/common/oauth2/v2.0/token",
data={
"grant_type": "refresh_token",
"client_id": client_id,
"client_secret": client_secret,
"refresh_token": refresh_token,
"scope": scope,
},
timeout=10,
)
r.raise_for_status()
return r.json()

0 comments on commit 946fdab

Please sign in to comment.