diff --git a/backend/onedrive/onedrive.go b/backend/onedrive/onedrive.go index 29eb270ff..980eec8c9 100644 --- a/backend/onedrive/onedrive.go +++ b/backend/onedrive/onedrive.go @@ -64,13 +64,20 @@ const ( // Globals var ( - authPath = "/common/oauth2/v2.0/authorize" - tokenPath = "/common/oauth2/v2.0/token" + + // Define the paths used for token operations + commonPathPrefix = "/common" // prefix for the paths if tenant isn't known + authPath = "/oauth2/v2.0/authorize" + tokenPath = "/oauth2/v2.0/token" scopeAccess = fs.SpaceSepList{"Files.Read", "Files.ReadWrite", "Files.Read.All", "Files.ReadWrite.All", "Sites.Read.All", "offline_access"} scopeAccessWithoutSites = fs.SpaceSepList{"Files.Read", "Files.ReadWrite", "Files.Read.All", "Files.ReadWrite.All", "offline_access"} - // Description of how to auth for this app for a business account + // When using client credential OAuth flow, scope of .default is required in order + // to use the permissions configured for the application within the tenant + scopeAccessClientCred = fs.SpaceSepList{".default"} + + // Base config for how to auth oauthConfig = &oauthutil.Config{ Scopes: scopeAccess, ClientID: rcloneClientID, @@ -182,6 +189,14 @@ Choose or manually enter a custom space separated list with all scopes, that rcl Help: "Read and write access to all resources, without the ability to browse SharePoint sites. \nSame as if disable_site_permission was set to true", }, }, + }, { + Name: "tenant", + Help: `ID of the service principal's tenant. Also called its directory ID. + +Set this if using +- Client Credential flow +`, + Sensitive: true, }, { Name: "disable_site_permission", Help: `Disable the request for Sites.Read.All permission. @@ -526,27 +541,54 @@ func chooseDrive(ctx context.Context, name string, m configmap.Mapper, srv *rest }) } +// Make the oauth config for the backend +func makeOauthConfig(ctx context.Context, opt *Options) (*oauthutil.Config, error) { + // Copy the default oauthConfig + oauthConfig := *oauthConfig + + // Set the scopes + oauthConfig.Scopes = opt.AccessScopes + if opt.DisableSitePermission { + oauthConfig.Scopes = scopeAccessWithoutSites + } + + // Construct the auth URLs + prefix := commonPathPrefix + if opt.Tenant != "" { + prefix = "/" + opt.Tenant + } + oauthConfig.TokenURL = authEndpoint[opt.Region] + prefix + tokenPath + oauthConfig.AuthURL = authEndpoint[opt.Region] + prefix + authPath + + // Check to see if we are using client credentials flow + if opt.ClientCredentials { + // Override scope to .default + oauthConfig.Scopes = scopeAccessClientCred + if opt.Tenant == "" { + return nil, fmt.Errorf("tenant parameter must be set when using %s", config.ConfigClientCredentials) + } + } + + return &oauthConfig, nil +} + // Config the backend -func Config(ctx context.Context, name string, m configmap.Mapper, config fs.ConfigIn) (*fs.ConfigOut, error) { - region, graphURL := getRegionURL(m) +func Config(ctx context.Context, name string, m configmap.Mapper, conf fs.ConfigIn) (*fs.ConfigOut, error) { + opt := new(Options) + err := configstruct.Set(m, opt) + if err != nil { + return nil, err + } + _, graphURL := getRegionURL(m) - if config.State == "" { - var accessScopes fs.SpaceSepList - accessScopesString, _ := m.Get("access_scopes") - err := accessScopes.Set(accessScopesString) + // Check to see if this is the start of the state machine execution + if conf.State == "" { + conf, err := makeOauthConfig(ctx, opt) if err != nil { - return nil, fmt.Errorf("failed to parse access_scopes: %w", err) + return nil, err } - oauthConfig.Scopes = []string(accessScopes) - disableSitePermission, _ := m.Get("disable_site_permission") - if disableSitePermission == "true" { - oauthConfig.Scopes = scopeAccessWithoutSites - } - oauthConfig.TokenURL = authEndpoint[region] + tokenPath - oauthConfig.AuthURL = authEndpoint[region] + authPath - return oauthutil.ConfigOut("choose_type", &oauthutil.Options{ - OAuth2Config: oauthConfig, + OAuth2Config: conf, }) } @@ -554,9 +596,11 @@ func Config(ctx context.Context, name string, m configmap.Mapper, config fs.Conf if err != nil { return nil, fmt.Errorf("failed to configure OneDrive: %w", err) } + + // Create a REST client, build on the OAuth client created above srv := rest.NewClient(oAuthClient) - switch config.State { + switch conf.State { case "choose_type": return fs.ConfigChooseExclusiveFixed("choose_type_done", "config_type", "Type of connection", []fs.OptionExample{{ Value: "onedrive", @@ -582,7 +626,7 @@ func Config(ctx context.Context, name string, m configmap.Mapper, config fs.Conf }}) case "choose_type_done": // Jump to next state according to config chosen - return fs.ConfigGoto(config.Result) + return fs.ConfigGoto(conf.Result) case "onedrive": return chooseDrive(ctx, name, m, srv, chooseDriveOpt{ opts: rest.Opts{ @@ -600,16 +644,22 @@ func Config(ctx context.Context, name string, m configmap.Mapper, config fs.Conf }, }) case "driveid": - return fs.ConfigInput("driveid_end", "config_driveid_fixed", "Drive ID") + out, err := fs.ConfigInput("driveid_end", "config_driveid_fixed", "Drive ID") + if err != nil { + return out, err + } + // Default the drive_id to the previous version in the config + out.Option.Default, _ = m.Get("drive_id") + return out, nil case "driveid_end": return chooseDrive(ctx, name, m, srv, chooseDriveOpt{ - finalDriveID: config.Result, + finalDriveID: conf.Result, }) case "siteid": return fs.ConfigInput("siteid_end", "config_siteid", "Site ID") case "siteid_end": return chooseDrive(ctx, name, m, srv, chooseDriveOpt{ - siteID: config.Result, + siteID: conf.Result, }) case "url": return fs.ConfigInput("url_end", "config_site_url", `Site URL @@ -620,7 +670,7 @@ Examples: - "https://XXX.sharepoint.com/teams/ID" `) case "url_end": - siteURL := config.Result + siteURL := conf.Result re := regexp.MustCompile(`https://.*\.sharepoint\.com(/.*)`) match := re.FindStringSubmatch(siteURL) if len(match) == 2 { @@ -635,12 +685,12 @@ Examples: return fs.ConfigInput("path_end", "config_sharepoint_url", `Server-relative URL`) case "path_end": return chooseDrive(ctx, name, m, srv, chooseDriveOpt{ - relativePath: config.Result, + relativePath: conf.Result, }) case "search": return fs.ConfigInput("search_end", "config_search_term", `Search term`) case "search_end": - searchTerm := config.Result + searchTerm := conf.Result opts := rest.Opts{ Method: "GET", RootURL: graphURL, @@ -662,10 +712,10 @@ Examples: }) case "search_sites": return chooseDrive(ctx, name, m, srv, chooseDriveOpt{ - siteID: config.Result, + siteID: conf.Result, }) case "driveid_final": - finalDriveID := config.Result + finalDriveID := conf.Result // Test the driveID and get drive type opts := rest.Opts{ @@ -684,12 +734,12 @@ Examples: return fs.ConfigConfirm("driveid_final_end", true, "config_drive_ok", fmt.Sprintf("Drive OK?\n\nFound drive %q of type %q\nURL: %s\n", rootItem.Name, rootItem.ParentReference.DriveType, rootItem.WebURL)) case "driveid_final_end": - if config.Result == "true" { + if conf.Result == "true" { return nil, nil } return fs.ConfigGoto("choose_type") } - return nil, fmt.Errorf("unknown state %q", config.State) + return nil, fmt.Errorf("unknown state %q", conf.State) } // Options defines the configuration for this backend @@ -700,7 +750,9 @@ type Options struct { DriveType string `config:"drive_type"` RootFolderID string `config:"root_folder_id"` DisableSitePermission bool `config:"disable_site_permission"` + ClientCredentials bool `config:"client_credentials"` AccessScopes fs.SpaceSepList `config:"access_scopes"` + Tenant string `config:"tenant"` ExposeOneNoteFiles bool `config:"expose_onenote_files"` ServerSideAcrossConfigs bool `config:"server_side_across_configs"` ListChunk int64 `config:"list_chunk"` @@ -988,12 +1040,11 @@ func NewFs(ctx context.Context, name, root string, m configmap.Mapper) (fs.Fs, e } rootURL := graphAPIEndpoint[opt.Region] + "/v1.0" + "/drives/" + opt.DriveID - oauthConfig.Scopes = opt.AccessScopes - if opt.DisableSitePermission { - oauthConfig.Scopes = scopeAccessWithoutSites + + oauthConfig, err := makeOauthConfig(ctx, opt) + if err != nil { + return nil, err } - oauthConfig.AuthURL = authEndpoint[opt.Region] + authPath - oauthConfig.TokenURL = authEndpoint[opt.Region] + tokenPath client := fshttp.NewClient(ctx) root = parsePath(root) @@ -2559,8 +2610,11 @@ func (o *Object) Update(ctx context.Context, in io.Reader, src fs.ObjectInfo, op return errors.New("can't upload content to a OneNote file") } - o.fs.tokenRenewer.Start() - defer o.fs.tokenRenewer.Stop() + // Only start the renewer if we have a valid one + if o.fs.tokenRenewer != nil { + o.fs.tokenRenewer.Start() + defer o.fs.tokenRenewer.Stop() + } size := src.Size() diff --git a/docs/content/onedrive.md b/docs/content/onedrive.md index 26242ba10..b2e599986 100644 --- a/docs/content/onedrive.md +++ b/docs/content/onedrive.md @@ -161,6 +161,27 @@ You may try to [verify you account](https://docs.microsoft.com/en-us/azure/activ Note: If you have a special region, you may need a different host in step 4 and 5. Here are [some hints](https://github.com/rclone/rclone/blob/bc23bf11db1c78c6ebbf8ea538fbebf7058b4176/backend/onedrive/onedrive.go#L86). +### Using OAuth Client Credential flow + +OAuth Client Credential flow will allow rclone to use permissions +directly associated with the Azure AD Enterprise application, rather +that adopting the context of an Azure AD user account. + +This flow can be enabled by following the steps below: + +1. Create the Enterprise App registration in the Azure AD portal and obtain a Client ID and Client Secret as described above. +2. Ensure that the application has the appropriate permissions and they are assigned as *Application Permissions* +3. Configure the remote, ensuring that *Client ID* and *Client Secret* are entered correctly. +4. In the *Advanced Config* section, enter `true` for `client_credentials` and in the `tenant` section enter the tenant ID. + +When it comes to choosing the type of the connection work with the +client credentials flow. In particular the "onedrive" option does not +work. You can use the "sharepoint" option or if that does not find the +correct drive ID type it in manually with the "driveid" option. + +**NOTE** Assigning permissions directly to the application means that +anyone with the *Client ID* and *Client Secret* can access your +OneDrive files. Take care to safeguard these credentials. ### Modification times and hashes