github.com/cs3org/reva/v2@v2.27.7/pkg/auth/manager/nextcloud/nextcloud.go (about)

     1  // Copyright 2018-2021 CERN
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  //
    15  // In applying this license, CERN does not waive the privileges and immunities
    16  // granted to it by virtue of its status as an Intergovernmental Organization
    17  // or submit itself to any jurisdiction.
    18  
    19  // Package nextcloud verifies a clientID and clientSecret against a Nextcloud backend.
    20  package nextcloud
    21  
    22  import (
    23  	"context"
    24  	"encoding/json"
    25  	"io"
    26  	"net/http"
    27  	"strings"
    28  
    29  	authpb "github.com/cs3org/go-cs3apis/cs3/auth/provider/v1beta1"
    30  	user "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
    31  	"github.com/cs3org/reva/v2/pkg/appctx"
    32  	"github.com/cs3org/reva/v2/pkg/auth"
    33  	"github.com/cs3org/reva/v2/pkg/auth/manager/registry"
    34  	"github.com/mitchellh/mapstructure"
    35  	"github.com/pkg/errors"
    36  )
    37  
    38  func init() {
    39  	registry.Register("nextcloud", New)
    40  }
    41  
    42  // Manager is the Nextcloud-based implementation of the auth.Manager interface
    43  // see https://github.com/cs3org/reva/blob/v1.13.0/pkg/auth/auth.go#L32-L35
    44  type Manager struct {
    45  	client       *http.Client
    46  	sharedSecret string
    47  	endPoint     string
    48  }
    49  
    50  // AuthManagerConfig contains config for a Nextcloud-based AuthManager
    51  type AuthManagerConfig struct {
    52  	EndPoint     string `mapstructure:"endpoint" docs:";The Nextcloud backend endpoint for user check"`
    53  	SharedSecret string `mapstructure:"shared_secret"`
    54  	MockHTTP     bool   `mapstructure:"mock_http"`
    55  }
    56  
    57  // Action describes a REST request to forward to the Nextcloud backend
    58  type Action struct {
    59  	verb     string
    60  	username string
    61  	argS     string
    62  }
    63  
    64  func (c *AuthManagerConfig) init() {
    65  }
    66  
    67  func parseConfig(m map[string]interface{}) (*AuthManagerConfig, error) {
    68  	c := &AuthManagerConfig{}
    69  	if err := mapstructure.Decode(m, c); err != nil {
    70  		err = errors.Wrap(err, "error decoding conf")
    71  		return nil, err
    72  	}
    73  	return c, nil
    74  }
    75  
    76  // New returns an auth manager implementation that verifies against a Nextcloud backend.
    77  func New(m map[string]interface{}) (auth.Manager, error) {
    78  	c, err := parseConfig(m)
    79  	if err != nil {
    80  		return nil, err
    81  	}
    82  	c.init()
    83  
    84  	return NewAuthManager(c)
    85  }
    86  
    87  // NewAuthManager returns a new Nextcloud-based AuthManager
    88  func NewAuthManager(c *AuthManagerConfig) (*Manager, error) {
    89  	var client *http.Client
    90  	if c.MockHTTP {
    91  		// called := make([]string, 0)
    92  		// nextcloudServerMock := GetNextcloudServerMock(&called)
    93  		// client, _ = TestingHTTPClient(nextcloudServerMock)
    94  
    95  		// Wait for SetHTTPClient to be called later
    96  		client = nil
    97  	} else {
    98  		if len(c.EndPoint) == 0 {
    99  			return nil, errors.New("Please specify 'endpoint' in '[grpc.services.authprovider.auth_managers.nextcloud]'")
   100  		}
   101  		client = &http.Client{}
   102  	}
   103  
   104  	return &Manager{
   105  		endPoint:     c.EndPoint, // e.g. "http://nc/apps/sciencemesh/"
   106  		sharedSecret: c.SharedSecret,
   107  		client:       client,
   108  	}, nil
   109  }
   110  
   111  // Configure method as defined in https://github.com/cs3org/reva/blob/v1.13.0/pkg/auth/auth.go#L32-L35
   112  func (am *Manager) Configure(ml map[string]interface{}) error {
   113  	return nil
   114  }
   115  
   116  // SetHTTPClient sets the HTTP client
   117  func (am *Manager) SetHTTPClient(c *http.Client) {
   118  	am.client = c
   119  }
   120  
   121  func (am *Manager) do(ctx context.Context, a Action) (int, []byte, error) {
   122  	log := appctx.GetLogger(ctx)
   123  	url := am.endPoint + "~" + a.username + "/api/auth/" + a.verb
   124  	log.Info().Msgf("am.do %s %s %s", url, a.argS, am.sharedSecret)
   125  	req, err := http.NewRequest(http.MethodPost, url, strings.NewReader(a.argS))
   126  	if err != nil {
   127  		return 0, nil, err
   128  	}
   129  	req.Header.Set("X-Reva-Secret", am.sharedSecret)
   130  
   131  	req.Header.Set("Content-Type", "application/json")
   132  	resp, err := am.client.Do(req)
   133  	if err != nil {
   134  		return 0, nil, err
   135  	}
   136  
   137  	defer resp.Body.Close()
   138  	body, err := io.ReadAll(resp.Body)
   139  	if err != nil {
   140  		return 0, nil, err
   141  	}
   142  
   143  	log.Info().Msgf("am.do response %d %s", resp.StatusCode, body)
   144  	return resp.StatusCode, body, nil
   145  }
   146  
   147  // Authenticate method as defined in https://github.com/cs3org/reva/blob/28500a8/pkg/auth/auth.go#L31-L33
   148  func (am *Manager) Authenticate(ctx context.Context, clientID, clientSecret string) (*user.User, map[string]*authpb.Scope, error) {
   149  	type paramsObj struct {
   150  		ClientID     string `json:"clientID"`
   151  		ClientSecret string `json:"clientSecret"`
   152  		// Scope        authpb.Scope
   153  	}
   154  	bodyObj := &paramsObj{
   155  		ClientID:     clientID,
   156  		ClientSecret: clientSecret,
   157  		// Scope: authpb.Scope{
   158  		// 	Resource: &types.OpaqueEntry{
   159  		// 		Decoder: "json",
   160  		// 		Value:   []byte(`{"resource_id":{"storage_id":"storage-id","opaque_id":"opaque-id"},"path":"some/file/path.txt"}`),
   161  		// 	},
   162  		// 	Role: authpb.Role_ROLE_OWNER,
   163  		// },
   164  	}
   165  	bodyStr, err := json.Marshal(bodyObj)
   166  	if err != nil {
   167  		return nil, nil, err
   168  	}
   169  	log := appctx.GetLogger(ctx)
   170  	log.Info().Msgf("Authenticate %s %s", clientID, bodyStr)
   171  
   172  	statusCode, body, err := am.do(ctx, Action{"Authenticate", clientID, string(bodyStr)})
   173  
   174  	if err != nil {
   175  		return nil, nil, err
   176  	}
   177  
   178  	if statusCode != 200 {
   179  		return nil, nil, errors.New("Username/password not recognized by Nextcloud backend")
   180  	}
   181  
   182  	type resultsObj struct {
   183  		User   user.User                `json:"user"`
   184  		Scopes map[string]*authpb.Scope `json:"scopes"`
   185  	}
   186  	result := &resultsObj{}
   187  	err = json.Unmarshal(body, &result)
   188  	if err != nil {
   189  		return nil, nil, err
   190  	}
   191  	var pointersMap = make(map[string]*authpb.Scope)
   192  	for k := range result.Scopes {
   193  		scope := result.Scopes[k]
   194  		pointersMap[k] = scope
   195  	}
   196  	return &result.User, pointersMap, nil
   197  }