github.com/cs3org/reva/v2@v2.27.7/pkg/cbox/group/rest/rest.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 rest
    20  
    21  import (
    22  	"context"
    23  	"errors"
    24  	"fmt"
    25  	"os"
    26  	"os/signal"
    27  	"strings"
    28  	"syscall"
    29  	"time"
    30  
    31  	grouppb "github.com/cs3org/go-cs3apis/cs3/identity/group/v1beta1"
    32  	userpb "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
    33  	"github.com/cs3org/reva/v2/pkg/appctx"
    34  	utils "github.com/cs3org/reva/v2/pkg/cbox/utils"
    35  	"github.com/cs3org/reva/v2/pkg/group"
    36  	"github.com/cs3org/reva/v2/pkg/group/manager/registry"
    37  	"github.com/gomodule/redigo/redis"
    38  	"github.com/mitchellh/mapstructure"
    39  	"github.com/rs/zerolog/log"
    40  )
    41  
    42  func init() {
    43  	registry.Register("rest", New)
    44  }
    45  
    46  type manager struct {
    47  	conf            *config
    48  	redisPool       *redis.Pool
    49  	apiTokenManager *utils.APITokenManager
    50  }
    51  
    52  type config struct {
    53  	// The address at which the redis server is running
    54  	RedisAddress string `mapstructure:"redis_address" docs:"localhost:6379"`
    55  	// The username for connecting to the redis server
    56  	RedisUsername string `mapstructure:"redis_username" docs:""`
    57  	// The password for connecting to the redis server
    58  	RedisPassword string `mapstructure:"redis_password" docs:""`
    59  	// The time in minutes for which the members of a group would be cached
    60  	GroupMembersCacheExpiration int `mapstructure:"group_members_cache_expiration" docs:"5"`
    61  	// The OIDC Provider
    62  	IDProvider string `mapstructure:"id_provider" docs:"http://cernbox.cern.ch"`
    63  	// Base API Endpoint
    64  	APIBaseURL string `mapstructure:"api_base_url" docs:"https://authorization-service-api-dev.web.cern.ch"`
    65  	// Client ID needed to authenticate
    66  	ClientID string `mapstructure:"client_id" docs:"-"`
    67  	// Client Secret
    68  	ClientSecret string `mapstructure:"client_secret" docs:"-"`
    69  
    70  	// Endpoint to generate token to access the API
    71  	OIDCTokenEndpoint string `mapstructure:"oidc_token_endpoint" docs:"https://keycloak-dev.cern.ch/auth/realms/cern/api-access/token"`
    72  	// The target application for which token needs to be generated
    73  	TargetAPI string `mapstructure:"target_api" docs:"authorization-service-api"`
    74  	// The time in seconds between bulk fetch of groups
    75  	GroupFetchInterval int `mapstructure:"group_fetch_interval" docs:"3600"`
    76  }
    77  
    78  func (c *config) init() {
    79  	if c.GroupMembersCacheExpiration == 0 {
    80  		c.GroupMembersCacheExpiration = 5
    81  	}
    82  	if c.RedisAddress == "" {
    83  		c.RedisAddress = ":6379"
    84  	}
    85  	if c.APIBaseURL == "" {
    86  		c.APIBaseURL = "https://authorization-service-api-dev.web.cern.ch"
    87  	}
    88  	if c.TargetAPI == "" {
    89  		c.TargetAPI = "authorization-service-api"
    90  	}
    91  	if c.OIDCTokenEndpoint == "" {
    92  		c.OIDCTokenEndpoint = "https://keycloak-dev.cern.ch/auth/realms/cern/api-access/token"
    93  	}
    94  	if c.IDProvider == "" {
    95  		c.IDProvider = "http://cernbox.cern.ch"
    96  	}
    97  	if c.GroupFetchInterval == 0 {
    98  		c.GroupFetchInterval = 3600
    99  	}
   100  }
   101  
   102  func parseConfig(m map[string]interface{}) (*config, error) {
   103  	c := &config{}
   104  	if err := mapstructure.Decode(m, c); err != nil {
   105  		return nil, err
   106  	}
   107  	return c, nil
   108  }
   109  
   110  // New returns a user manager implementation that makes calls to the GRAPPA API.
   111  func New(m map[string]interface{}) (group.Manager, error) {
   112  	c, err := parseConfig(m)
   113  	if err != nil {
   114  		return nil, err
   115  	}
   116  	c.init()
   117  
   118  	redisPool := initRedisPool(c.RedisAddress, c.RedisUsername, c.RedisPassword)
   119  	apiTokenManager := utils.InitAPITokenManager(c.TargetAPI, c.OIDCTokenEndpoint, c.ClientID, c.ClientSecret)
   120  
   121  	mgr := &manager{
   122  		conf:            c,
   123  		redisPool:       redisPool,
   124  		apiTokenManager: apiTokenManager,
   125  	}
   126  	go mgr.fetchAllGroups()
   127  	return mgr, nil
   128  }
   129  
   130  func (m *manager) fetchAllGroups() {
   131  	_ = m.fetchAllGroupAccounts()
   132  	ticker := time.NewTicker(time.Duration(m.conf.GroupFetchInterval) * time.Second)
   133  	work := make(chan os.Signal, 1)
   134  	signal.Notify(work, syscall.SIGHUP, syscall.SIGINT, syscall.SIGQUIT)
   135  
   136  	for {
   137  		select {
   138  		case <-work:
   139  			return
   140  		case <-ticker.C:
   141  			_ = m.fetchAllGroupAccounts()
   142  		}
   143  	}
   144  }
   145  
   146  func (m *manager) fetchAllGroupAccounts() error {
   147  	ctx := context.Background()
   148  	url := fmt.Sprintf("%s/api/v1.0/Group?field=groupIdentifier&field=displayName&field=gid", m.conf.APIBaseURL)
   149  
   150  	for url != "" {
   151  		result, err := m.apiTokenManager.SendAPIGetRequest(ctx, url, false)
   152  		if err != nil {
   153  			return err
   154  		}
   155  
   156  		responseData, ok := result["data"].([]interface{})
   157  		if !ok {
   158  			return errors.New("rest: error in type assertion")
   159  		}
   160  		for _, usr := range responseData {
   161  			groupData, ok := usr.(map[string]interface{})
   162  			if !ok {
   163  				continue
   164  			}
   165  
   166  			_, err = m.parseAndCacheGroup(ctx, groupData)
   167  			if err != nil {
   168  				continue
   169  			}
   170  		}
   171  
   172  		url = ""
   173  		if pagination, ok := result["pagination"].(map[string]interface{}); ok {
   174  			if links, ok := pagination["links"].(map[string]interface{}); ok {
   175  				if next, ok := links["next"].(string); ok {
   176  					url = fmt.Sprintf("%s%s", m.conf.APIBaseURL, next)
   177  				}
   178  			}
   179  		}
   180  	}
   181  
   182  	return nil
   183  }
   184  
   185  func (m *manager) parseAndCacheGroup(ctx context.Context, groupData map[string]interface{}) (*grouppb.Group, error) {
   186  	id, ok := groupData["groupIdentifier"].(string)
   187  	if !ok {
   188  		return nil, errors.New("rest: missing upn in user data")
   189  	}
   190  
   191  	name, _ := groupData["displayName"].(string)
   192  	groupID := &grouppb.GroupId{
   193  		OpaqueId: id,
   194  		Idp:      m.conf.IDProvider,
   195  	}
   196  	gid, ok := groupData["gid"].(int64)
   197  	if !ok {
   198  		gid = 0
   199  	}
   200  	g := &grouppb.Group{
   201  		Id:          groupID,
   202  		GroupName:   id,
   203  		Mail:        id + "@cern.ch",
   204  		DisplayName: name,
   205  		GidNumber:   gid,
   206  	}
   207  
   208  	if err := m.cacheGroupDetails(g); err != nil {
   209  		log.Error().Err(err).Msg("rest: error caching group details")
   210  	}
   211  
   212  	if internalID, ok := groupData["id"].(string); ok {
   213  		if err := m.cacheInternalID(groupID, internalID); err != nil {
   214  			log.Error().Err(err).Msg("rest: error caching group details")
   215  		}
   216  	}
   217  
   218  	return g, nil
   219  
   220  }
   221  
   222  func (m *manager) GetGroup(ctx context.Context, gid *grouppb.GroupId, skipFetchingMembers bool) (*grouppb.Group, error) {
   223  	g, err := m.fetchCachedGroupDetails(gid)
   224  	if err != nil {
   225  		return nil, err
   226  	}
   227  
   228  	if !skipFetchingMembers {
   229  		groupMembers, err := m.GetMembers(ctx, gid)
   230  		if err != nil {
   231  			return nil, err
   232  		}
   233  		g.Members = groupMembers
   234  	}
   235  
   236  	return g, nil
   237  }
   238  
   239  func (m *manager) GetGroupByClaim(ctx context.Context, claim, value string, skipFetchingMembers bool) (*grouppb.Group, error) {
   240  	if claim == "group_name" {
   241  		return m.GetGroup(ctx, &grouppb.GroupId{OpaqueId: value}, skipFetchingMembers)
   242  	}
   243  
   244  	g, err := m.fetchCachedGroupByParam(claim, value)
   245  	if err != nil {
   246  		return nil, err
   247  	}
   248  
   249  	if !skipFetchingMembers {
   250  		groupMembers, err := m.GetMembers(ctx, g.Id)
   251  		if err != nil {
   252  			return nil, err
   253  		}
   254  		g.Members = groupMembers
   255  	}
   256  
   257  	return g, nil
   258  }
   259  
   260  func (m *manager) FindGroups(ctx context.Context, query string, skipFetchingMembers bool) ([]*grouppb.Group, error) {
   261  
   262  	// Look at namespaces filters. If the query starts with:
   263  	// "a" or none => get egroups
   264  	// other filters => get empty list
   265  
   266  	parts := strings.SplitN(query, ":", 2)
   267  
   268  	if len(parts) == 2 {
   269  		if parts[0] == "a" {
   270  			query = parts[1]
   271  		} else {
   272  			return []*grouppb.Group{}, nil
   273  		}
   274  	}
   275  
   276  	return m.findCachedGroups(query)
   277  }
   278  
   279  func (m *manager) GetMembers(ctx context.Context, gid *grouppb.GroupId) ([]*userpb.UserId, error) {
   280  
   281  	users, err := m.fetchCachedGroupMembers(gid)
   282  	if err == nil {
   283  		return users, nil
   284  	}
   285  
   286  	internalID, err := m.fetchCachedInternalID(gid)
   287  	if err != nil {
   288  		return nil, err
   289  	}
   290  	url := fmt.Sprintf("%s/api/v1.0/Group/%s/memberidentities/precomputed", m.conf.APIBaseURL, internalID)
   291  	result, err := m.apiTokenManager.SendAPIGetRequest(ctx, url, false)
   292  	if err != nil {
   293  		return nil, err
   294  	}
   295  
   296  	userData := result["data"].([]interface{})
   297  	users = []*userpb.UserId{}
   298  
   299  	for _, u := range userData {
   300  		userInfo, ok := u.(map[string]interface{})
   301  		if !ok {
   302  			return nil, errors.New("rest: error in type assertion")
   303  		}
   304  		if id, ok := userInfo["upn"].(string); ok {
   305  			users = append(users, &userpb.UserId{OpaqueId: id, Idp: m.conf.IDProvider})
   306  		}
   307  	}
   308  
   309  	if err = m.cacheGroupMembers(gid, users); err != nil {
   310  		log := appctx.GetLogger(ctx)
   311  		log.Error().Err(err).Msg("rest: error caching group members")
   312  	}
   313  
   314  	return users, nil
   315  }
   316  
   317  func (m *manager) HasMember(ctx context.Context, gid *grouppb.GroupId, uid *userpb.UserId) (bool, error) {
   318  	groupMemers, err := m.GetMembers(ctx, gid)
   319  	if err != nil {
   320  		return false, err
   321  	}
   322  
   323  	for _, u := range groupMemers {
   324  		if uid.OpaqueId == u.OpaqueId {
   325  			return true, nil
   326  		}
   327  	}
   328  	return false, nil
   329  }