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 }