github.com/minio/minio@v0.0.0-20240328213742-3f72439b8a27/cmd/iam-object-store.go (about) 1 // Copyright (c) 2015-2021 MinIO, Inc. 2 // 3 // This file is part of MinIO Object Storage stack 4 // 5 // This program is free software: you can redistribute it and/or modify 6 // it under the terms of the GNU Affero General Public License as published by 7 // the Free Software Foundation, either version 3 of the License, or 8 // (at your option) any later version. 9 // 10 // This program is distributed in the hope that it will be useful 11 // but WITHOUT ANY WARRANTY; without even the implied warranty of 12 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 // GNU Affero General Public License for more details. 14 // 15 // You should have received a copy of the GNU Affero General Public License 16 // along with this program. If not, see <http://www.gnu.org/licenses/>. 17 18 package cmd 19 20 import ( 21 "bytes" 22 "context" 23 "errors" 24 "fmt" 25 "path" 26 "strings" 27 "sync" 28 "time" 29 "unicode/utf8" 30 31 jsoniter "github.com/json-iterator/go" 32 "github.com/minio/madmin-go/v3" 33 "github.com/minio/minio-go/v7/pkg/set" 34 "github.com/minio/minio/internal/config" 35 xioutil "github.com/minio/minio/internal/ioutil" 36 "github.com/minio/minio/internal/kms" 37 "github.com/minio/minio/internal/logger" 38 "github.com/puzpuzpuz/xsync/v3" 39 ) 40 41 // IAMObjectStore implements IAMStorageAPI 42 type IAMObjectStore struct { 43 // Protect access to storage within the current server. 44 sync.RWMutex 45 46 *iamCache 47 48 usersSysType UsersSysType 49 50 objAPI ObjectLayer 51 } 52 53 func newIAMObjectStore(objAPI ObjectLayer, usersSysType UsersSysType) *IAMObjectStore { 54 return &IAMObjectStore{ 55 iamCache: newIamCache(), 56 objAPI: objAPI, 57 usersSysType: usersSysType, 58 } 59 } 60 61 func (iamOS *IAMObjectStore) rlock() *iamCache { 62 iamOS.RLock() 63 return iamOS.iamCache 64 } 65 66 func (iamOS *IAMObjectStore) runlock() { 67 iamOS.RUnlock() 68 } 69 70 func (iamOS *IAMObjectStore) lock() *iamCache { 71 iamOS.Lock() 72 return iamOS.iamCache 73 } 74 75 func (iamOS *IAMObjectStore) unlock() { 76 iamOS.Unlock() 77 } 78 79 func (iamOS *IAMObjectStore) getUsersSysType() UsersSysType { 80 return iamOS.usersSysType 81 } 82 83 func (iamOS *IAMObjectStore) saveIAMConfig(ctx context.Context, item interface{}, objPath string, opts ...options) error { 84 json := jsoniter.ConfigCompatibleWithStandardLibrary 85 data, err := json.Marshal(item) 86 if err != nil { 87 return err 88 } 89 if GlobalKMS != nil { 90 data, err = config.EncryptBytes(GlobalKMS, data, kms.Context{ 91 minioMetaBucket: path.Join(minioMetaBucket, objPath), 92 }) 93 if err != nil { 94 return err 95 } 96 } 97 return saveConfig(ctx, iamOS.objAPI, objPath, data) 98 } 99 100 func decryptData(data []byte, objPath string) ([]byte, error) { 101 if utf8.Valid(data) { 102 return data, nil 103 } 104 105 pdata, err := madmin.DecryptData(globalActiveCred.String(), bytes.NewReader(data)) 106 if err == nil { 107 return pdata, nil 108 } 109 if GlobalKMS != nil { 110 pdata, err = config.DecryptBytes(GlobalKMS, data, kms.Context{ 111 minioMetaBucket: path.Join(minioMetaBucket, objPath), 112 }) 113 if err == nil { 114 return pdata, nil 115 } 116 pdata, err = config.DecryptBytes(GlobalKMS, data, kms.Context{ 117 minioMetaBucket: objPath, 118 }) 119 if err == nil { 120 return pdata, nil 121 } 122 } 123 return nil, err 124 } 125 126 func (iamOS *IAMObjectStore) loadIAMConfigBytesWithMetadata(ctx context.Context, objPath string) ([]byte, ObjectInfo, error) { 127 data, meta, err := readConfigWithMetadata(ctx, iamOS.objAPI, objPath, ObjectOptions{}) 128 if err != nil { 129 return nil, meta, err 130 } 131 data, err = decryptData(data, objPath) 132 if err != nil { 133 return nil, meta, err 134 } 135 return data, meta, nil 136 } 137 138 func (iamOS *IAMObjectStore) loadIAMConfig(ctx context.Context, item interface{}, objPath string) error { 139 data, _, err := iamOS.loadIAMConfigBytesWithMetadata(ctx, objPath) 140 if err != nil { 141 return err 142 } 143 json := jsoniter.ConfigCompatibleWithStandardLibrary 144 return json.Unmarshal(data, item) 145 } 146 147 func (iamOS *IAMObjectStore) deleteIAMConfig(ctx context.Context, path string) error { 148 return deleteConfig(ctx, iamOS.objAPI, path) 149 } 150 151 func (iamOS *IAMObjectStore) loadPolicyDocWithRetry(ctx context.Context, policy string, m map[string]PolicyDoc, retries int) error { 152 for { 153 retry: 154 data, objInfo, err := iamOS.loadIAMConfigBytesWithMetadata(ctx, getPolicyDocPath(policy)) 155 if err != nil { 156 if err == errConfigNotFound { 157 return errNoSuchPolicy 158 } 159 retries-- 160 if retries <= 0 { 161 return err 162 } 163 time.Sleep(500 * time.Millisecond) 164 goto retry 165 } 166 167 var p PolicyDoc 168 err = p.parseJSON(data) 169 if err != nil { 170 return err 171 } 172 173 if p.Version == 0 { 174 // This means that policy was in the old version (without any 175 // timestamp info). We fetch the mod time of the file and save 176 // that as create and update date. 177 p.CreateDate = objInfo.ModTime 178 p.UpdateDate = objInfo.ModTime 179 } 180 181 m[policy] = p 182 return nil 183 } 184 } 185 186 func (iamOS *IAMObjectStore) loadPolicyDoc(ctx context.Context, policy string, m map[string]PolicyDoc) error { 187 data, objInfo, err := iamOS.loadIAMConfigBytesWithMetadata(ctx, getPolicyDocPath(policy)) 188 if err != nil { 189 if err == errConfigNotFound { 190 return errNoSuchPolicy 191 } 192 return err 193 } 194 195 var p PolicyDoc 196 err = p.parseJSON(data) 197 if err != nil { 198 return err 199 } 200 201 if p.Version == 0 { 202 // This means that policy was in the old version (without any 203 // timestamp info). We fetch the mod time of the file and save 204 // that as create and update date. 205 p.CreateDate = objInfo.ModTime 206 p.UpdateDate = objInfo.ModTime 207 } 208 209 m[policy] = p 210 return nil 211 } 212 213 func (iamOS *IAMObjectStore) loadPolicyDocs(ctx context.Context, m map[string]PolicyDoc) error { 214 ctx, cancel := context.WithCancel(ctx) 215 defer cancel() 216 for item := range listIAMConfigItems(ctx, iamOS.objAPI, iamConfigPoliciesPrefix) { 217 if item.Err != nil { 218 return item.Err 219 } 220 221 policyName := path.Dir(item.Item) 222 if err := iamOS.loadPolicyDoc(ctx, policyName, m); err != nil && !errors.Is(err, errNoSuchPolicy) { 223 return err 224 } 225 } 226 return nil 227 } 228 229 func (iamOS *IAMObjectStore) loadUser(ctx context.Context, user string, userType IAMUserType, m map[string]UserIdentity) error { 230 var u UserIdentity 231 err := iamOS.loadIAMConfig(ctx, &u, getUserIdentityPath(user, userType)) 232 if err != nil { 233 if err == errConfigNotFound { 234 return errNoSuchUser 235 } 236 return err 237 } 238 239 if u.Credentials.IsExpired() { 240 // Delete expired identity - ignoring errors here. 241 iamOS.deleteIAMConfig(ctx, getUserIdentityPath(user, userType)) 242 iamOS.deleteIAMConfig(ctx, getMappedPolicyPath(user, userType, false)) 243 return nil 244 } 245 246 if u.Credentials.AccessKey == "" { 247 u.Credentials.AccessKey = user 248 } 249 250 if u.Credentials.SessionToken != "" { 251 jwtClaims, err := extractJWTClaims(u) 252 if err != nil { 253 if u.Credentials.IsTemp() { 254 // We should delete such that the client can re-request 255 // for the expiring credentials. 256 iamOS.deleteIAMConfig(ctx, getUserIdentityPath(user, userType)) 257 iamOS.deleteIAMConfig(ctx, getMappedPolicyPath(user, userType, false)) 258 return nil 259 } 260 return err 261 262 } 263 u.Credentials.Claims = jwtClaims.Map() 264 } 265 266 if u.Credentials.Description == "" { 267 u.Credentials.Description = u.Credentials.Comment 268 } 269 270 m[user] = u 271 return nil 272 } 273 274 func (iamOS *IAMObjectStore) loadUsers(ctx context.Context, userType IAMUserType, m map[string]UserIdentity) error { 275 var basePrefix string 276 switch userType { 277 case svcUser: 278 basePrefix = iamConfigServiceAccountsPrefix 279 case stsUser: 280 basePrefix = iamConfigSTSPrefix 281 default: 282 basePrefix = iamConfigUsersPrefix 283 } 284 285 ctx, cancel := context.WithCancel(ctx) 286 defer cancel() 287 for item := range listIAMConfigItems(ctx, iamOS.objAPI, basePrefix) { 288 if item.Err != nil { 289 return item.Err 290 } 291 292 userName := path.Dir(item.Item) 293 if err := iamOS.loadUser(ctx, userName, userType, m); err != nil && err != errNoSuchUser { 294 return err 295 } 296 } 297 return nil 298 } 299 300 func (iamOS *IAMObjectStore) loadGroup(ctx context.Context, group string, m map[string]GroupInfo) error { 301 var g GroupInfo 302 err := iamOS.loadIAMConfig(ctx, &g, getGroupInfoPath(group)) 303 if err != nil { 304 if err == errConfigNotFound { 305 return errNoSuchGroup 306 } 307 return err 308 } 309 m[group] = g 310 return nil 311 } 312 313 func (iamOS *IAMObjectStore) loadGroups(ctx context.Context, m map[string]GroupInfo) error { 314 ctx, cancel := context.WithCancel(ctx) 315 defer cancel() 316 for item := range listIAMConfigItems(ctx, iamOS.objAPI, iamConfigGroupsPrefix) { 317 if item.Err != nil { 318 return item.Err 319 } 320 321 group := path.Dir(item.Item) 322 if err := iamOS.loadGroup(ctx, group, m); err != nil && err != errNoSuchGroup { 323 return err 324 } 325 } 326 return nil 327 } 328 329 func (iamOS *IAMObjectStore) loadMappedPolicyWithRetry(ctx context.Context, name string, userType IAMUserType, isGroup bool, m *xsync.MapOf[string, MappedPolicy], retries int) error { 330 for { 331 retry: 332 var p MappedPolicy 333 err := iamOS.loadIAMConfig(ctx, &p, getMappedPolicyPath(name, userType, isGroup)) 334 if err != nil { 335 if err == errConfigNotFound { 336 return errNoSuchPolicy 337 } 338 retries-- 339 if retries <= 0 { 340 return err 341 } 342 time.Sleep(500 * time.Millisecond) 343 goto retry 344 } 345 346 m.Store(name, p) 347 return nil 348 } 349 } 350 351 func (iamOS *IAMObjectStore) loadMappedPolicy(ctx context.Context, name string, userType IAMUserType, isGroup bool, m *xsync.MapOf[string, MappedPolicy]) error { 352 var p MappedPolicy 353 err := iamOS.loadIAMConfig(ctx, &p, getMappedPolicyPath(name, userType, isGroup)) 354 if err != nil { 355 if err == errConfigNotFound { 356 return errNoSuchPolicy 357 } 358 return err 359 } 360 361 m.Store(name, p) 362 return nil 363 } 364 365 func (iamOS *IAMObjectStore) loadMappedPolicies(ctx context.Context, userType IAMUserType, isGroup bool, m *xsync.MapOf[string, MappedPolicy]) error { 366 var basePath string 367 if isGroup { 368 basePath = iamConfigPolicyDBGroupsPrefix 369 } else { 370 switch userType { 371 case svcUser: 372 basePath = iamConfigPolicyDBServiceAccountsPrefix 373 case stsUser: 374 basePath = iamConfigPolicyDBSTSUsersPrefix 375 default: 376 basePath = iamConfigPolicyDBUsersPrefix 377 } 378 } 379 ctx, cancel := context.WithCancel(ctx) 380 defer cancel() 381 for item := range listIAMConfigItems(ctx, iamOS.objAPI, basePath) { 382 if item.Err != nil { 383 return item.Err 384 } 385 386 policyFile := item.Item 387 userOrGroupName := strings.TrimSuffix(policyFile, ".json") 388 if err := iamOS.loadMappedPolicy(ctx, userOrGroupName, userType, isGroup, m); err != nil && !errors.Is(err, errNoSuchPolicy) { 389 return err 390 } 391 } 392 return nil 393 } 394 395 var ( 396 usersListKey = "users/" 397 svcAccListKey = "service-accounts/" 398 groupsListKey = "groups/" 399 policiesListKey = "policies/" 400 stsListKey = "sts/" 401 policyDBUsersListKey = "policydb/users/" 402 policyDBSTSUsersListKey = "policydb/sts-users/" 403 policyDBServiceAccountsListKey = "policydb/service-accounts/" 404 policyDBGroupsListKey = "policydb/groups/" 405 406 // List of directories from which to read iam data into memory. 407 allListKeys = []string{ 408 usersListKey, 409 svcAccListKey, 410 groupsListKey, 411 policiesListKey, 412 stsListKey, 413 policyDBUsersListKey, 414 policyDBSTSUsersListKey, 415 policyDBServiceAccountsListKey, 416 policyDBGroupsListKey, 417 } 418 419 // List of directories to skip: we do not read STS directories for better 420 // performance. STS credentials would be stored in memory when they are 421 // first used. 422 iamLoadSkipListKeySet = set.CreateStringSet( 423 stsListKey, 424 policyDBSTSUsersListKey, 425 ) 426 ) 427 428 func (iamOS *IAMObjectStore) listAllIAMConfigItems(ctx context.Context) (map[string][]string, error) { 429 res := make(map[string][]string) 430 ctx, cancel := context.WithCancel(ctx) 431 defer cancel() 432 for _, listKey := range allListKeys { 433 if iamLoadSkipListKeySet.Contains(listKey) { 434 continue 435 } 436 for item := range listIAMConfigItems(ctx, iamOS.objAPI, iamConfigPrefix+SlashSeparator+listKey) { 437 if item.Err != nil { 438 return nil, item.Err 439 } 440 res[listKey] = append(res[listKey], item.Item) 441 } 442 } 443 return res, nil 444 } 445 446 // PurgeExpiredSTS - purge expired STS credentials from object store. 447 func (iamOS *IAMObjectStore) PurgeExpiredSTS(ctx context.Context) error { 448 if iamOS.objAPI == nil { 449 return errServerNotInitialized 450 } 451 452 bootstrapTraceMsg("purging expired STS credentials") 453 // Scan STS users on disk and purge expired ones. We do not need to hold a 454 // lock with store.lock() here. 455 for item := range listIAMConfigItems(ctx, iamOS.objAPI, iamConfigPrefix+SlashSeparator+stsListKey) { 456 if item.Err != nil { 457 return item.Err 458 } 459 userName := path.Dir(item.Item) 460 // loadUser() will delete expired user during the load - we do not need 461 // to keep the loaded user around in memory, so we reinitialize the map 462 // each time. 463 m := map[string]UserIdentity{} 464 if err := iamOS.loadUser(ctx, userName, stsUser, m); err != nil && err != errNoSuchUser { 465 logger.LogIf(GlobalContext, fmt.Errorf("unable to load user during STS purge: %w (%s)", err, item.Item)) 466 } 467 468 } 469 return nil 470 } 471 472 // Assumes cache is locked by caller. 473 func (iamOS *IAMObjectStore) loadAllFromObjStore(ctx context.Context, cache *iamCache) error { 474 if iamOS.objAPI == nil { 475 return errServerNotInitialized 476 } 477 478 bootstrapTraceMsg("loading all IAM items") 479 480 listedConfigItems, err := iamOS.listAllIAMConfigItems(ctx) 481 if err != nil { 482 return fmt.Errorf("unable to list IAM data: %w", err) 483 } 484 485 // Loads things in the same order as `LoadIAMCache()` 486 487 bootstrapTraceMsg("loading policy documents") 488 489 policiesList := listedConfigItems[policiesListKey] 490 for _, item := range policiesList { 491 policyName := path.Dir(item) 492 if err := iamOS.loadPolicyDoc(ctx, policyName, cache.iamPolicyDocsMap); err != nil && !errors.Is(err, errNoSuchPolicy) { 493 return fmt.Errorf("unable to load the policy doc `%s`: %w", policyName, err) 494 } 495 } 496 setDefaultCannedPolicies(cache.iamPolicyDocsMap) 497 498 if iamOS.usersSysType == MinIOUsersSysType { 499 bootstrapTraceMsg("loading regular IAM users") 500 regUsersList := listedConfigItems[usersListKey] 501 for _, item := range regUsersList { 502 userName := path.Dir(item) 503 if err := iamOS.loadUser(ctx, userName, regUser, cache.iamUsersMap); err != nil && err != errNoSuchUser { 504 return fmt.Errorf("unable to load the user `%s`: %w", userName, err) 505 } 506 } 507 508 bootstrapTraceMsg("loading regular IAM groups") 509 groupsList := listedConfigItems[groupsListKey] 510 for _, item := range groupsList { 511 group := path.Dir(item) 512 if err := iamOS.loadGroup(ctx, group, cache.iamGroupsMap); err != nil && err != errNoSuchGroup { 513 return fmt.Errorf("unable to load the group `%s`: %w", group, err) 514 } 515 } 516 } 517 518 bootstrapTraceMsg("loading user policy mapping") 519 userPolicyMappingsList := listedConfigItems[policyDBUsersListKey] 520 for _, item := range userPolicyMappingsList { 521 userName := strings.TrimSuffix(item, ".json") 522 if err := iamOS.loadMappedPolicy(ctx, userName, regUser, false, cache.iamUserPolicyMap); err != nil && !errors.Is(err, errNoSuchPolicy) { 523 return fmt.Errorf("unable to load the policy mapping for the user `%s`: %w", userName, err) 524 } 525 } 526 527 bootstrapTraceMsg("loading group policy mapping") 528 groupPolicyMappingsList := listedConfigItems[policyDBGroupsListKey] 529 for _, item := range groupPolicyMappingsList { 530 groupName := strings.TrimSuffix(item, ".json") 531 if err := iamOS.loadMappedPolicy(ctx, groupName, regUser, true, cache.iamGroupPolicyMap); err != nil && !errors.Is(err, errNoSuchPolicy) { 532 return fmt.Errorf("unable to load the policy mapping for the group `%s`: %w", groupName, err) 533 } 534 } 535 536 bootstrapTraceMsg("loading service accounts") 537 svcAccList := listedConfigItems[svcAccListKey] 538 svcUsersMap := make(map[string]UserIdentity, len(svcAccList)) 539 for _, item := range svcAccList { 540 userName := path.Dir(item) 541 if err := iamOS.loadUser(ctx, userName, svcUser, svcUsersMap); err != nil && err != errNoSuchUser { 542 return fmt.Errorf("unable to load the service account `%s`: %w", userName, err) 543 } 544 } 545 for _, svcAcc := range svcUsersMap { 546 svcParent := svcAcc.Credentials.ParentUser 547 if _, ok := cache.iamUsersMap[svcParent]; !ok { 548 // If a service account's parent user is not in iamUsersMap, the 549 // parent is an STS account. Such accounts may have a policy mapped 550 // on the parent user, so we load them. This is not needed for the 551 // initial server startup, however, it is needed for the case where 552 // the STS account's policy mapping (for example in LDAP mode) may 553 // be changed and the user's policy mapping in memory is stale 554 // (because the policy change notification was missed by the current 555 // server). 556 // 557 // The "policy not found" error is ignored because the STS account may 558 // not have a policy mapped via its parent (for e.g. in 559 // OIDC/AssumeRoleWithCustomToken/AssumeRoleWithCertificate). 560 err := iamOS.loadMappedPolicy(ctx, svcParent, stsUser, false, cache.iamSTSPolicyMap) 561 if err != nil && !errors.Is(err, errNoSuchPolicy) { 562 return fmt.Errorf("unable to load the policy mapping for the STS user `%s`: %w", svcParent, err) 563 } 564 } 565 } 566 // Copy svcUsersMap to cache.iamUsersMap 567 for k, v := range svcUsersMap { 568 cache.iamUsersMap[k] = v 569 } 570 571 cache.buildUserGroupMemberships() 572 return nil 573 } 574 575 func (iamOS *IAMObjectStore) savePolicyDoc(ctx context.Context, policyName string, p PolicyDoc) error { 576 return iamOS.saveIAMConfig(ctx, &p, getPolicyDocPath(policyName)) 577 } 578 579 func (iamOS *IAMObjectStore) saveMappedPolicy(ctx context.Context, name string, userType IAMUserType, isGroup bool, mp MappedPolicy, opts ...options) error { 580 return iamOS.saveIAMConfig(ctx, mp, getMappedPolicyPath(name, userType, isGroup), opts...) 581 } 582 583 func (iamOS *IAMObjectStore) saveUserIdentity(ctx context.Context, name string, userType IAMUserType, u UserIdentity, opts ...options) error { 584 return iamOS.saveIAMConfig(ctx, u, getUserIdentityPath(name, userType), opts...) 585 } 586 587 func (iamOS *IAMObjectStore) saveGroupInfo(ctx context.Context, name string, gi GroupInfo) error { 588 return iamOS.saveIAMConfig(ctx, gi, getGroupInfoPath(name)) 589 } 590 591 func (iamOS *IAMObjectStore) deletePolicyDoc(ctx context.Context, name string) error { 592 err := iamOS.deleteIAMConfig(ctx, getPolicyDocPath(name)) 593 if err == errConfigNotFound { 594 err = errNoSuchPolicy 595 } 596 return err 597 } 598 599 func (iamOS *IAMObjectStore) deleteMappedPolicy(ctx context.Context, name string, userType IAMUserType, isGroup bool) error { 600 err := iamOS.deleteIAMConfig(ctx, getMappedPolicyPath(name, userType, isGroup)) 601 if err == errConfigNotFound { 602 err = errNoSuchPolicy 603 } 604 return err 605 } 606 607 func (iamOS *IAMObjectStore) deleteUserIdentity(ctx context.Context, name string, userType IAMUserType) error { 608 err := iamOS.deleteIAMConfig(ctx, getUserIdentityPath(name, userType)) 609 if err == errConfigNotFound { 610 err = errNoSuchUser 611 } 612 return err 613 } 614 615 func (iamOS *IAMObjectStore) deleteGroupInfo(ctx context.Context, name string) error { 616 err := iamOS.deleteIAMConfig(ctx, getGroupInfoPath(name)) 617 if err == errConfigNotFound { 618 err = errNoSuchGroup 619 } 620 return err 621 } 622 623 // helper type for listIAMConfigItems 624 type itemOrErr struct { 625 Item string 626 Err error 627 } 628 629 // Lists files or dirs in the minioMetaBucket at the given path 630 // prefix. If dirs is true, only directories are listed, otherwise 631 // only objects are listed. All returned items have the pathPrefix 632 // removed from their names. 633 func listIAMConfigItems(ctx context.Context, objAPI ObjectLayer, pathPrefix string) <-chan itemOrErr { 634 ch := make(chan itemOrErr) 635 636 go func() { 637 defer xioutil.SafeClose(ch) 638 639 // Allocate new results channel to receive ObjectInfo. 640 objInfoCh := make(chan ObjectInfo) 641 642 if err := objAPI.Walk(ctx, minioMetaBucket, pathPrefix, objInfoCh, WalkOptions{}); err != nil { 643 select { 644 case ch <- itemOrErr{Err: err}: 645 case <-ctx.Done(): 646 } 647 return 648 } 649 650 for obj := range objInfoCh { 651 item := strings.TrimPrefix(obj.Name, pathPrefix) 652 item = strings.TrimSuffix(item, SlashSeparator) 653 select { 654 case ch <- itemOrErr{Item: item}: 655 case <-ctx.Done(): 656 return 657 } 658 } 659 }() 660 661 return ch 662 }