- Added new functionality to resolve external URLs for files on Google Drive and Microsoft OneDrive mounts. - Introduced `mount_cloud_service.go` to handle OAuth token extraction and URL resolution. - Enhanced `mounts_service.go` to update mount configurations with OAuth tokens. - Updated API routes to include a new endpoint for fetching external URLs. - Implemented enrichment functions in `cloud_native.go` to mark files that should open in the provider's web editor. - Added tests for cloud-native file enrichment in `cloud_native_test.go` to ensure correct behavior.
311 lines
8.6 KiB
Go
311 lines
8.6 KiB
Go
package drive
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"strings"
|
|
|
|
"github.com/ultisuite/ulti-backend/internal/drivestore"
|
|
"github.com/ultisuite/ulti-backend/internal/nextcloud"
|
|
"github.com/ultisuite/ulti-backend/internal/orgpolicy"
|
|
)
|
|
|
|
var ErrOAuthNotConfigured = errors.New("oauth provider not configured")
|
|
|
|
type MountView struct {
|
|
ID string `json:"id"`
|
|
Scope string `json:"scope"`
|
|
OrgSlug *string `json:"org_slug,omitempty"`
|
|
DisplayName string `json:"display_name"`
|
|
BackendType string `json:"backend_type"`
|
|
MountPoint string `json:"mount_point"`
|
|
Status string `json:"status"`
|
|
LastError string `json:"last_error,omitempty"`
|
|
NCMountID *int `json:"nc_mount_id,omitempty"`
|
|
NeedsOAuth bool `json:"needs_oauth,omitempty"`
|
|
}
|
|
|
|
type CreateMountParams struct {
|
|
Scope string
|
|
OrgSlug string
|
|
DisplayName string
|
|
BackendType string
|
|
WebDAV *nextcloud.WebDAVMountConfig
|
|
OAuthBackend string
|
|
OAuthAuth string
|
|
}
|
|
|
|
func oauthProviderForBackend(backendType string) (providerKey, ncBackend, authBackend string, isOAuth bool) {
|
|
switch strings.TrimSpace(strings.ToLower(backendType)) {
|
|
case "googledrive", "google":
|
|
return orgpolicy.MountOAuthProviderGoogle, "googledrive", "oauth2::google", true
|
|
case "dropbox":
|
|
return orgpolicy.MountOAuthProviderDropbox, "dropbox", "oauth2::dropbox", true
|
|
case "onedrive", "microsoft":
|
|
return orgpolicy.MountOAuthProviderMicrosoft, "onedrive", "oauth2::microsoft", true
|
|
default:
|
|
return "", "", "", false
|
|
}
|
|
}
|
|
|
|
func (s *Service) orgPolicyLoader() *orgpolicy.Loader {
|
|
return orgpolicy.NewLoader(s.db, nil)
|
|
}
|
|
|
|
func (s *Service) ListMountsForUser(ctx context.Context, platformUserID, ncUserID string, orgSlugs []string) ([]MountView, error) {
|
|
store := s.ensureStore()
|
|
if store == nil {
|
|
return nil, fmt.Errorf("store not configured")
|
|
}
|
|
mounts, err := store.ListMountsForUser(ctx, platformUserID, orgSlugs)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
out := make([]MountView, 0, len(mounts))
|
|
for _, m := range mounts {
|
|
out = append(out, mapMountView(m))
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
func mapMountView(m drivestore.Mount) MountView {
|
|
view := MountView{
|
|
ID: m.ID,
|
|
Scope: m.Scope,
|
|
OrgSlug: m.OrgSlug,
|
|
DisplayName: m.DisplayName,
|
|
BackendType: m.BackendType,
|
|
MountPoint: m.MountPoint,
|
|
Status: m.Status,
|
|
LastError: m.LastError,
|
|
NCMountID: m.NCMountID,
|
|
}
|
|
if m.Status == "pending_oauth" {
|
|
view.NeedsOAuth = true
|
|
}
|
|
return view
|
|
}
|
|
|
|
func (s *Service) CreateMount(ctx context.Context, platformUserID, ncUserID string, p CreateMountParams) (MountView, error) {
|
|
store := s.ensureStore()
|
|
if store == nil {
|
|
return MountView{}, fmt.Errorf("store not configured")
|
|
}
|
|
displayName := strings.TrimSpace(p.DisplayName)
|
|
backendType := strings.TrimSpace(strings.ToLower(p.BackendType))
|
|
if displayName == "" || backendType == "" {
|
|
return MountView{}, ErrInvalid
|
|
}
|
|
mountPoint := "/" + strings.Trim(displayName, "/")
|
|
scope := strings.TrimSpace(strings.ToLower(p.Scope))
|
|
if scope != "user" && scope != "org" {
|
|
return MountView{}, ErrInvalid
|
|
}
|
|
|
|
var ncMountID int
|
|
var err error
|
|
var configEnc []byte
|
|
status := "active"
|
|
|
|
providerKey, ncBackend, authBackend, isOAuth := oauthProviderForBackend(backendType)
|
|
if isOAuth {
|
|
creds, err := s.orgPolicyLoader().MountOAuthCredentials(ctx, providerKey)
|
|
if err != nil {
|
|
return MountView{}, err
|
|
}
|
|
if !creds.Enabled {
|
|
return MountView{}, ErrOAuthNotConfigured
|
|
}
|
|
oauthConfig := map[string]string{
|
|
"client_id": creds.ClientID,
|
|
"client_secret": creds.ClientSecret,
|
|
"configured": "false",
|
|
"token": "",
|
|
}
|
|
configEnc, _ = json.Marshal(oauthConfig)
|
|
if scope == "org" {
|
|
return MountView{}, ErrInvalid
|
|
}
|
|
ncMountID, err = s.nc.CreateOAuthExternalMount(ctx, ncUserID, mountPoint, ncBackend, authBackend, oauthConfig)
|
|
status = "pending_oauth"
|
|
} else {
|
|
switch backendType {
|
|
case "webdav", "dav":
|
|
if p.WebDAV == nil {
|
|
return MountView{}, ErrInvalid
|
|
}
|
|
configEnc, _ = json.Marshal(p.WebDAV)
|
|
if scope == "org" {
|
|
ncMountID, err = s.nc.CreateGlobalWebDAVMount(ctx, mountPoint, *p.WebDAV)
|
|
} else {
|
|
ncMountID, err = s.nc.CreateUserWebDAVMount(ctx, ncUserID, mountPoint, *p.WebDAV)
|
|
}
|
|
case "googledrive", "google", "dropbox", "onedrive", "microsoft":
|
|
return MountView{}, ErrOAuthNotConfigured
|
|
default:
|
|
if p.OAuthBackend != "" {
|
|
auth := p.OAuthAuth
|
|
if auth == "" {
|
|
auth = "oauth2::" + backendType
|
|
}
|
|
ncMountID, err = s.nc.CreateOAuthExternalMount(ctx, ncUserID, mountPoint, p.OAuthBackend, auth, nil)
|
|
status = "pending_oauth"
|
|
} else {
|
|
return MountView{}, ErrInvalid
|
|
}
|
|
}
|
|
}
|
|
|
|
lastError := ""
|
|
if err != nil {
|
|
status = "error"
|
|
lastError = err.Error()
|
|
}
|
|
|
|
var ownerID *string
|
|
var orgSlug *string
|
|
if scope == "user" {
|
|
ownerID = &platformUserID
|
|
} else {
|
|
slug := strings.TrimSpace(strings.ToLower(p.OrgSlug))
|
|
if slug == "" {
|
|
return MountView{}, ErrInvalid
|
|
}
|
|
orgSlug = &slug
|
|
}
|
|
|
|
var ncIDPtr *int
|
|
if ncMountID > 0 {
|
|
ncIDPtr = &ncMountID
|
|
}
|
|
row, err := store.CreateMount(ctx, drivestore.CreateMountParams{
|
|
Scope: scope,
|
|
OwnerUserID: ownerID,
|
|
OrgSlug: orgSlug,
|
|
NCMountID: ncIDPtr,
|
|
DisplayName: displayName,
|
|
BackendType: backendType,
|
|
MountPoint: mountPoint,
|
|
Status: status,
|
|
ConfigEnc: configEnc,
|
|
})
|
|
if err != nil {
|
|
if ncMountID > 0 {
|
|
_ = s.nc.DeleteExternalMount(ctx, ncMountID)
|
|
}
|
|
return MountView{}, err
|
|
}
|
|
if status == "error" {
|
|
_ = store.UpdateMountStatus(ctx, row.ID, status, lastError, ncIDPtr)
|
|
}
|
|
return mapMountView(row), nil
|
|
}
|
|
|
|
func (s *Service) DeleteMount(ctx context.Context, mountID string) error {
|
|
store := s.ensureStore()
|
|
if store == nil {
|
|
return fmt.Errorf("store not configured")
|
|
}
|
|
mount, err := store.GetMount(ctx, mountID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if mount.NCMountID != nil && *mount.NCMountID > 0 {
|
|
if err := s.nc.DeleteExternalMount(ctx, *mount.NCMountID); err != nil {
|
|
return mapDriveError(err)
|
|
}
|
|
}
|
|
return store.DeleteMount(ctx, mountID)
|
|
}
|
|
|
|
func (s *Service) GetMountOAuthURL(ctx context.Context, mountID, platformUserID, ncUserID, redirectURI string) (string, error) {
|
|
if err := validateMountOAuthRedirectURI(redirectURI); err != nil {
|
|
return "", err
|
|
}
|
|
store := s.ensureStore()
|
|
if store == nil {
|
|
return "", fmt.Errorf("store not configured")
|
|
}
|
|
mount, err := store.GetMount(ctx, mountID)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
if mount.OwnerUserID == nil || *mount.OwnerUserID != platformUserID {
|
|
return "", ErrForbidden
|
|
}
|
|
if mount.NCMountID == nil {
|
|
return "", ErrInvalid
|
|
}
|
|
providerKey, _, _, isOAuth := oauthProviderForBackend(mount.BackendType)
|
|
if !isOAuth {
|
|
return "", ErrInvalid
|
|
}
|
|
creds, err := s.orgPolicyLoader().MountOAuthCredentials(ctx, providerKey)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
if !creds.Enabled {
|
|
return "", ErrOAuthNotConfigured
|
|
}
|
|
return s.nc.StartExternalStorageOAuth2(ctx, ncUserID, creds.ClientID, creds.ClientSecret, redirectURI)
|
|
}
|
|
|
|
func (s *Service) CompleteMountOAuth(ctx context.Context, mountID, platformUserID, ncUserID, redirectURI, code string) error {
|
|
if err := validateMountOAuthRedirectURI(redirectURI); err != nil {
|
|
return err
|
|
}
|
|
store := s.ensureStore()
|
|
if store == nil {
|
|
return fmt.Errorf("store not configured")
|
|
}
|
|
code = strings.TrimSpace(code)
|
|
if code == "" {
|
|
return ErrInvalid
|
|
}
|
|
mount, err := store.GetMount(ctx, mountID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if mount.OwnerUserID == nil || *mount.OwnerUserID != platformUserID {
|
|
return ErrForbidden
|
|
}
|
|
if mount.NCMountID == nil {
|
|
return ErrInvalid
|
|
}
|
|
providerKey, _, _, isOAuth := oauthProviderForBackend(mount.BackendType)
|
|
if !isOAuth {
|
|
return ErrInvalid
|
|
}
|
|
creds, err := s.orgPolicyLoader().MountOAuthCredentials(ctx, providerKey)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if !creds.Enabled {
|
|
return ErrOAuthNotConfigured
|
|
}
|
|
token, err := s.nc.CompleteExternalStorageOAuth2(ctx, ncUserID, creds.ClientID, creds.ClientSecret, redirectURI, code)
|
|
if err != nil {
|
|
_ = store.UpdateMountStatus(ctx, mount.ID, "error", err.Error(), mount.NCMountID)
|
|
return err
|
|
}
|
|
if err := s.nc.UpdateUserExternalMountOAuth(ctx, ncUserID, *mount.NCMountID, creds.ClientID, creds.ClientSecret, token); err != nil {
|
|
_ = store.UpdateMountStatus(ctx, mount.ID, "error", err.Error(), mount.NCMountID)
|
|
return err
|
|
}
|
|
configEnc, _ := store.GetMountConfig(ctx, mount.ID)
|
|
cfg := map[string]any{}
|
|
if len(configEnc) > 0 {
|
|
_ = json.Unmarshal(configEnc, &cfg)
|
|
}
|
|
cfg["token"] = token
|
|
cfg["configured"] = true
|
|
cfg["client_id"] = creds.ClientID
|
|
cfg["client_secret"] = creds.ClientSecret
|
|
if merged, err := json.Marshal(cfg); err == nil {
|
|
_ = store.UpdateMountConfig(ctx, mount.ID, merged)
|
|
}
|
|
return store.UpdateMountStatus(ctx, mount.ID, "active", "", mount.NCMountID)
|
|
}
|