mirror of
https://github.com/rclone/rclone.git
synced 2024-12-22 23:22:08 +01:00
onedrive: add support for OAuth client credential flow - fixes #6197
This adds support for the client credential flow oauth method which requires some special handling in onedrive: - Special scopes are required - The tenant is required - The tenant needs to be used in the oauth URLs This also: - refactors the oauth config creation so it isn't duplicated - defaults the drive_id to the previous one in the config - updates the documentation Co-authored-by: Nick Craig-Wood <nick@craig-wood.com>
This commit is contained in:
parent
65012beea4
commit
2f3e90f671
@ -64,13 +64,20 @@ const (
|
|||||||
|
|
||||||
// Globals
|
// Globals
|
||||||
var (
|
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"}
|
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"}
|
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{
|
oauthConfig = &oauthutil.Config{
|
||||||
Scopes: scopeAccess,
|
Scopes: scopeAccess,
|
||||||
ClientID: rcloneClientID,
|
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",
|
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",
|
Name: "disable_site_permission",
|
||||||
Help: `Disable the request for Sites.Read.All 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
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Config the backend
|
// Make the oauth config for the backend
|
||||||
func Config(ctx context.Context, name string, m configmap.Mapper, config fs.ConfigIn) (*fs.ConfigOut, error) {
|
func makeOauthConfig(ctx context.Context, opt *Options) (*oauthutil.Config, error) {
|
||||||
region, graphURL := getRegionURL(m)
|
// Copy the default oauthConfig
|
||||||
|
oauthConfig := *oauthConfig
|
||||||
|
|
||||||
if config.State == "" {
|
// Set the scopes
|
||||||
var accessScopes fs.SpaceSepList
|
oauthConfig.Scopes = opt.AccessScopes
|
||||||
accessScopesString, _ := m.Get("access_scopes")
|
if opt.DisableSitePermission {
|
||||||
err := accessScopes.Set(accessScopesString)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to parse access_scopes: %w", err)
|
|
||||||
}
|
|
||||||
oauthConfig.Scopes = []string(accessScopes)
|
|
||||||
disableSitePermission, _ := m.Get("disable_site_permission")
|
|
||||||
if disableSitePermission == "true" {
|
|
||||||
oauthConfig.Scopes = scopeAccessWithoutSites
|
oauthConfig.Scopes = scopeAccessWithoutSites
|
||||||
}
|
}
|
||||||
oauthConfig.TokenURL = authEndpoint[region] + tokenPath
|
|
||||||
oauthConfig.AuthURL = authEndpoint[region] + authPath
|
|
||||||
|
|
||||||
|
// 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, conf fs.ConfigIn) (*fs.ConfigOut, error) {
|
||||||
|
opt := new(Options)
|
||||||
|
err := configstruct.Set(m, opt)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
_, graphURL := getRegionURL(m)
|
||||||
|
|
||||||
|
// 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, err
|
||||||
|
}
|
||||||
return oauthutil.ConfigOut("choose_type", &oauthutil.Options{
|
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 {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to configure OneDrive: %w", err)
|
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)
|
srv := rest.NewClient(oAuthClient)
|
||||||
|
|
||||||
switch config.State {
|
switch conf.State {
|
||||||
case "choose_type":
|
case "choose_type":
|
||||||
return fs.ConfigChooseExclusiveFixed("choose_type_done", "config_type", "Type of connection", []fs.OptionExample{{
|
return fs.ConfigChooseExclusiveFixed("choose_type_done", "config_type", "Type of connection", []fs.OptionExample{{
|
||||||
Value: "onedrive",
|
Value: "onedrive",
|
||||||
@ -582,7 +626,7 @@ func Config(ctx context.Context, name string, m configmap.Mapper, config fs.Conf
|
|||||||
}})
|
}})
|
||||||
case "choose_type_done":
|
case "choose_type_done":
|
||||||
// Jump to next state according to config chosen
|
// Jump to next state according to config chosen
|
||||||
return fs.ConfigGoto(config.Result)
|
return fs.ConfigGoto(conf.Result)
|
||||||
case "onedrive":
|
case "onedrive":
|
||||||
return chooseDrive(ctx, name, m, srv, chooseDriveOpt{
|
return chooseDrive(ctx, name, m, srv, chooseDriveOpt{
|
||||||
opts: rest.Opts{
|
opts: rest.Opts{
|
||||||
@ -600,16 +644,22 @@ func Config(ctx context.Context, name string, m configmap.Mapper, config fs.Conf
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
case "driveid":
|
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":
|
case "driveid_end":
|
||||||
return chooseDrive(ctx, name, m, srv, chooseDriveOpt{
|
return chooseDrive(ctx, name, m, srv, chooseDriveOpt{
|
||||||
finalDriveID: config.Result,
|
finalDriveID: conf.Result,
|
||||||
})
|
})
|
||||||
case "siteid":
|
case "siteid":
|
||||||
return fs.ConfigInput("siteid_end", "config_siteid", "Site ID")
|
return fs.ConfigInput("siteid_end", "config_siteid", "Site ID")
|
||||||
case "siteid_end":
|
case "siteid_end":
|
||||||
return chooseDrive(ctx, name, m, srv, chooseDriveOpt{
|
return chooseDrive(ctx, name, m, srv, chooseDriveOpt{
|
||||||
siteID: config.Result,
|
siteID: conf.Result,
|
||||||
})
|
})
|
||||||
case "url":
|
case "url":
|
||||||
return fs.ConfigInput("url_end", "config_site_url", `Site URL
|
return fs.ConfigInput("url_end", "config_site_url", `Site URL
|
||||||
@ -620,7 +670,7 @@ Examples:
|
|||||||
- "https://XXX.sharepoint.com/teams/ID"
|
- "https://XXX.sharepoint.com/teams/ID"
|
||||||
`)
|
`)
|
||||||
case "url_end":
|
case "url_end":
|
||||||
siteURL := config.Result
|
siteURL := conf.Result
|
||||||
re := regexp.MustCompile(`https://.*\.sharepoint\.com(/.*)`)
|
re := regexp.MustCompile(`https://.*\.sharepoint\.com(/.*)`)
|
||||||
match := re.FindStringSubmatch(siteURL)
|
match := re.FindStringSubmatch(siteURL)
|
||||||
if len(match) == 2 {
|
if len(match) == 2 {
|
||||||
@ -635,12 +685,12 @@ Examples:
|
|||||||
return fs.ConfigInput("path_end", "config_sharepoint_url", `Server-relative URL`)
|
return fs.ConfigInput("path_end", "config_sharepoint_url", `Server-relative URL`)
|
||||||
case "path_end":
|
case "path_end":
|
||||||
return chooseDrive(ctx, name, m, srv, chooseDriveOpt{
|
return chooseDrive(ctx, name, m, srv, chooseDriveOpt{
|
||||||
relativePath: config.Result,
|
relativePath: conf.Result,
|
||||||
})
|
})
|
||||||
case "search":
|
case "search":
|
||||||
return fs.ConfigInput("search_end", "config_search_term", `Search term`)
|
return fs.ConfigInput("search_end", "config_search_term", `Search term`)
|
||||||
case "search_end":
|
case "search_end":
|
||||||
searchTerm := config.Result
|
searchTerm := conf.Result
|
||||||
opts := rest.Opts{
|
opts := rest.Opts{
|
||||||
Method: "GET",
|
Method: "GET",
|
||||||
RootURL: graphURL,
|
RootURL: graphURL,
|
||||||
@ -662,10 +712,10 @@ Examples:
|
|||||||
})
|
})
|
||||||
case "search_sites":
|
case "search_sites":
|
||||||
return chooseDrive(ctx, name, m, srv, chooseDriveOpt{
|
return chooseDrive(ctx, name, m, srv, chooseDriveOpt{
|
||||||
siteID: config.Result,
|
siteID: conf.Result,
|
||||||
})
|
})
|
||||||
case "driveid_final":
|
case "driveid_final":
|
||||||
finalDriveID := config.Result
|
finalDriveID := conf.Result
|
||||||
|
|
||||||
// Test the driveID and get drive type
|
// Test the driveID and get drive type
|
||||||
opts := rest.Opts{
|
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))
|
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":
|
case "driveid_final_end":
|
||||||
if config.Result == "true" {
|
if conf.Result == "true" {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
return fs.ConfigGoto("choose_type")
|
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
|
// Options defines the configuration for this backend
|
||||||
@ -700,7 +750,9 @@ type Options struct {
|
|||||||
DriveType string `config:"drive_type"`
|
DriveType string `config:"drive_type"`
|
||||||
RootFolderID string `config:"root_folder_id"`
|
RootFolderID string `config:"root_folder_id"`
|
||||||
DisableSitePermission bool `config:"disable_site_permission"`
|
DisableSitePermission bool `config:"disable_site_permission"`
|
||||||
|
ClientCredentials bool `config:"client_credentials"`
|
||||||
AccessScopes fs.SpaceSepList `config:"access_scopes"`
|
AccessScopes fs.SpaceSepList `config:"access_scopes"`
|
||||||
|
Tenant string `config:"tenant"`
|
||||||
ExposeOneNoteFiles bool `config:"expose_onenote_files"`
|
ExposeOneNoteFiles bool `config:"expose_onenote_files"`
|
||||||
ServerSideAcrossConfigs bool `config:"server_side_across_configs"`
|
ServerSideAcrossConfigs bool `config:"server_side_across_configs"`
|
||||||
ListChunk int64 `config:"list_chunk"`
|
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
|
rootURL := graphAPIEndpoint[opt.Region] + "/v1.0" + "/drives/" + opt.DriveID
|
||||||
oauthConfig.Scopes = opt.AccessScopes
|
|
||||||
if opt.DisableSitePermission {
|
oauthConfig, err := makeOauthConfig(ctx, opt)
|
||||||
oauthConfig.Scopes = scopeAccessWithoutSites
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
}
|
}
|
||||||
oauthConfig.AuthURL = authEndpoint[opt.Region] + authPath
|
|
||||||
oauthConfig.TokenURL = authEndpoint[opt.Region] + tokenPath
|
|
||||||
|
|
||||||
client := fshttp.NewClient(ctx)
|
client := fshttp.NewClient(ctx)
|
||||||
root = parsePath(root)
|
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")
|
return errors.New("can't upload content to a OneNote file")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Only start the renewer if we have a valid one
|
||||||
|
if o.fs.tokenRenewer != nil {
|
||||||
o.fs.tokenRenewer.Start()
|
o.fs.tokenRenewer.Start()
|
||||||
defer o.fs.tokenRenewer.Stop()
|
defer o.fs.tokenRenewer.Stop()
|
||||||
|
}
|
||||||
|
|
||||||
size := src.Size()
|
size := src.Size()
|
||||||
|
|
||||||
|
@ -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).
|
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
|
### Modification times and hashes
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user