Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 0 additions & 2 deletions .engine_cart.yml

This file was deleted.

2 changes: 2 additions & 0 deletions .rubocop.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ Metrics/ClassLength:
Max: 130
Exclude:
- 'lib/browse_everything/driver/google_drive.rb'
- 'lib/browse_everything/driver/sharepoint.rb'

Metrics/MethodLength:
Exclude:
Expand Down Expand Up @@ -79,6 +80,7 @@ Style/MixinUsage:
- 'spec/lib/browse_everything/driver/file_system_spec.rb'
- 'spec/lib/browse_everything/driver/google_drive_spec.rb'
- 'spec/lib/browse_everything/driver/s3_spec.rb'
- 'spec/lib/browse_everything/driver/sharepoint_spec.rb'
- 'spec/services/browser_factory_spec.rb'

Style/NumericLiterals:
Expand Down
12 changes: 5 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ Community Support: [![Samvera Community Slack](https://img.shields.io/badge/samv
This Gem allows your rails application to access user files from cloud storage.
Currently there are drivers implemented for [Dropbox](http://www.dropbox.com),
[Google Drive](http://drive.google.com),
[Box](http://www.box.com), [Amazon S3](https://aws.amazon.com/s3/),
[Box](http://www.box.com), [Amazon S3](https://aws.amazon.com/s3/), [Sharepoint](https://www.microsoft.com/en-us/microsoft-365/sharepoint/collaboration)
and a server-side directory share.

The gem uses [OAuth](http://oauth.net/) to connect to a user's account and
Expand Down Expand Up @@ -108,19 +108,17 @@ _Until a Product Owner has been identified, we ask that you please direct all re

## Supported Ruby Releases
Currently, the following releases of Ruby are tested:
- 3.3
- 3.2
- 3.1
- 3.0
- 2.7
- 2.6

## Supported Rails Releases
The supported Rail releases follow those specified by [the security policy of the Rails Community](https://rubyonrails.org/security/). As is the case with the supported Ruby releases, it is recommended that one upgrades from any Rails release no longer receiving security updates.
- 8.0
- 7.2
- 7.1
- 7.0
- 6.1
- 6.0
- 5.2
- 5.1

## Installation

Expand Down
28 changes: 28 additions & 0 deletions SharePoint.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# Sharepoint Provider

This provider will allow browse-everything to access Sharepoint on behalf of a specific user. It routes through the `/me/joinedTeams` and `/me/drives` Graph API endpoints. This will list Teams that the user belongs to and the user's personal drives. Within each Team it will expand to list any child drives or files that the user has permission to access.

https://learn.microsoft.com/en-us/graph/auth-v2-user?tabs=http

Prerequisite:
* App must be registered in the Entra Admin center to receive client_id, client_secret, and tenant_id.
* If using .default endpoint as your scope, you must register API permissions for your application. Minimum permissions:
* Files.Read
* Files.Read.All
* Files.Read.Selected
* offline_access
* openid
* profile
* Team.ReadBasic.All
* User.Read

To use the sharepoint provider add the following to `config/browse_everything_providers.yml`:

```
sharepoint:
client_id: MyAppClientID
client_secret: MyAppClientSecret
tenant_id: MyAzureTenantID
redirect_uri: https://example.com/browse/connect
scope: offline_access https://graph.microsoft.com/.default
```
3 changes: 2 additions & 1 deletion app/controllers/browse_everything_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ def provider
# Hence, a Browser must be reinstantiated for each request using the state provided in the Rails session
# @return [BrowseEverything::Browser]
def browser
BrowserFactory.build(session: session, url_options: url_options)
@browser ||= BrowserFactory.build(session: session, url_options: url_options)
end

helper_method :auth_link
Expand All @@ -129,4 +129,5 @@ def browser
helper_method :provider
helper_method :provider_name
helper_method :provider_contents
helper_method :reset_provider_session!
end
7 changes: 6 additions & 1 deletion app/views/browse_everything/index.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,12 @@
<div class="col-xs-12 col-12 ev-files list">
<% if provider.present? %>
<% if provider.authorized? %>
<%= render :partial => 'files' %>
<% begin %>
<%= render :partial => 'files' %>
<% rescue StandardError %>
<% reset_provider_session! %>
<%= render :partial => 'auth' %>
<% end %>
<% else %>
<%= render :partial => 'auth' %>
<% end %>
Expand Down
7 changes: 7 additions & 0 deletions lib/browse_everything.rb
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ module Driver
autoload :FileSystem, 'browse_everything/driver/file_system'
autoload :GoogleDrive, 'browse_everything/driver/google_drive'
autoload :S3, 'browse_everything/driver/s3'
autoload :Sharepoint, 'browse_everything/driver/sharepoint'

# Intentionally require explicit require, as it has a non-declared dependency
# autoload :Dropbox, 'browse_everything/driver/dropbox'
Expand Down Expand Up @@ -41,6 +42,12 @@ module Google
end
end

module Auth
module Sharepoint
autoload :Session, 'browse_everything/auth/sharepoint/session'
end
end

class InitializationError < RuntimeError; end
class ConfigurationError < StandardError; end
class NotImplementedError < StandardError; end
Expand Down
28 changes: 0 additions & 28 deletions lib/browse_everything/auth/google/credentials.rb

This file was deleted.

55 changes: 55 additions & 0 deletions lib/browse_everything/auth/sharepoint/session.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
# frozen_string_literal: true

require 'oauth2'

# BrowseEverything OAuth2 session for
# Sharepoint provider
module BrowseEverything
module Auth
module Sharepoint
class Session
OAUTH2_URLS = {
site: 'https://login.microsoftonline.com'
}.freeze

def initialize(opts = {})
token_info = opts[:access_token]&.symbolize_keys

if opts[:client_id]
@oauth2_client = OAuth2::Client.new(opts[:client_id],
opts[:client_secret],
{
authorize_url: authorize_url(opts[:tenant_id]),
token_url: token_url(opts[:tenant_id]),
redirect_uri: opts[:redirect_uri],
scope: opts[:scope]
}.merge!(OAUTH2_URLS.dup))
return if token_info.blank?
@access_token = OAuth2::AccessToken.new(@oauth2_client,
token_info[:token],
{
refresh_token: token_info[:refresh_token],
expires_in: token_info[:expires_in]
})
end
end

def authorize_url(tenant_id)
tenant_id + "/oauth2/v2.0/authorize"
end

def token_url(tenant_id)
tenant_id + "/oauth2/v2.0/token"
end

def get_access_token(code)
@access_token = @oauth2_client.auth_code.get_token(code)
end

def refresh_token
@access_token = @access_token.refresh!
end
end
end
end
end
67 changes: 58 additions & 9 deletions lib/browse_everything/driver/google_drive.rb
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ def token=(value)
value = value.fetch('access_token') if value.is_a? Hash

# Restore the credentials if the access token string itself has been cached
restore_credentials(value) if @credentials.nil?
@credentials = authorizer.get_credentials(user_id) || restore_credentials(value) if @credentials.nil?

super(value)
end
Expand Down Expand Up @@ -90,15 +90,64 @@ def list_files(drive, request_params, path: '')
@entries += list_files(drive, request_params, path: path) if request_params.page_token.present?
end

# Retrieve the drive details
# @param drive [Google::Apis::DriveV3::Drive] the Google Drive File
# @param path [String] path for the resource details (unused)
# @return [BrowseEverything::FileEntry] file entry for the resource node
def drive_details(drive)
BrowseEverything::FileEntry.new(
drive.id,
"#{key}:#{drive.id}",
drive.name,
0,
Time.new,
true,
'drive'
)
end

# Lists the drives accessible by a Google Drive context
# @param drive [Google::Apis::DriveV3::DriveService] the Google Drive context
# @return [Array<BrowseEverything::FileEntry>] file entries for the drives
def list_drives(drive)
page_token = nil
drive.list_drives(fields: "nextPageToken,drives(name,id)", page_size: 100) do |drive_list, error|
# Raise an exception if there was an error Google API's
if error.present?
# In order to properly trigger reauthentication, the token must be cleared
# Additionally, the error is not automatically raised from the Google Client
@token = nil
raise error
end

@entries += drive_list.drives.map do |gdrive_file|
drive_details(gdrive_file)
end

page_token = drive_list.next_page_token
end

@entries += list_drives(drive) if page_token.present?
end

# Retrieve the files for any given resource on Google Drive
# @param path [String] the root or Folder path for which to list contents
# @return [Array<BrowseEverything::FileEntry>] file entries for the path
def contents(path = '')
@entries = []
drive_service.batch do |drive|
request_params = Auth::Google::RequestParameters.new
request_params.q += " and '#{path}' in parents " if path.present?
list_files(drive, request_params, path: path)
if path.empty?
@entries << drive_details(Google::Apis::DriveV3::Drive.new(id: "root", name: "My Drive"))
@entries << drive_details(Google::Apis::DriveV3::Drive.new(id: "shared_drives", name: "Shared drives")) if drive_service.list_drives.drives.any?
elsif path == 'shared_drives'
drive_service.batch do |drive|
list_drives(drive)
end
else
drive_service.batch do |drive|
request_params = Auth::Google::RequestParameters.new
request_params.q += " and '#{path}' in parents "
list_files(drive, request_params, path: path)
end
end

@sorter.call(@entries)
Expand All @@ -108,7 +157,7 @@ def contents(path = '')
# @param id [String] identifier for the resource
# @return [Array<String, Hash>] authorized link to the resource
def link_for(id)
file = drive_service.get_file(id, fields: 'id, name, size')
file = drive_service.get_file(id, supports_all_drives: true, fields: 'id, name, size')
auth_header = { 'Authorization' => "Bearer #{credentials.access_token}" }
extras = {
auth_header: auth_header,
Expand Down Expand Up @@ -166,7 +215,7 @@ def authorizer
# This is *the* method which, passing an HTTP request, redeems an authorization code for an access token
# @return [String] a new access token
def authorize!
@credentials = authorizer.get_credentials_from_code(user_id: user_id, code: code)
@credentials = authorizer.get_and_store_credentials_from_code(user_id: user_id, code: code)
@token = @credentials.access_token
@code = nil # The authorization code can only be redeemed for an access token once
@token
Expand Down Expand Up @@ -229,11 +278,11 @@ def download_url(id)
# @param access_token [String] the access token redeemed using an authorization code
# @return Credentials credentials restored from a cached access token
def restore_credentials(access_token)
client = Auth::Google::Credentials.new
client = Google::Auth::UserRefreshCredentials.new
client.client_id = client_id.id
client.client_secret = client_id.secret
client.update_token!('access_token' => access_token)
@credentials = client
client
end
end
end
Expand Down
Loading