github.com/treeverse/lakefs@v1.24.1-0.20240520134607-95648127bfb0/pkg/kv/migrations/rbac_to_acl.go (about) 1 package migrations 2 3 import ( 4 "context" 5 "errors" 6 "fmt" 7 "strings" 8 "time" 9 10 "github.com/hashicorp/go-multierror" 11 "github.com/treeverse/lakefs/pkg/auth" 12 "github.com/treeverse/lakefs/pkg/auth/acl" 13 "github.com/treeverse/lakefs/pkg/auth/crypt" 14 "github.com/treeverse/lakefs/pkg/auth/model" 15 authparams "github.com/treeverse/lakefs/pkg/auth/params" 16 "github.com/treeverse/lakefs/pkg/auth/wildcard" 17 "github.com/treeverse/lakefs/pkg/config" 18 "github.com/treeverse/lakefs/pkg/kv" 19 "github.com/treeverse/lakefs/pkg/logging" 20 "github.com/treeverse/lakefs/pkg/permissions" 21 ) 22 23 const ( 24 maxGroups = 1000 25 pageSize = 1000 26 maxGroupPolicies = 1000 27 ) 28 29 var ( 30 // ErrTooMany is returned when this migration does not support a 31 // particular number of resources. It should not occur on any 32 // reasonably sized installation. 33 ErrTooMany = errors.New("too many") 34 ErrTooManyPolicies = fmt.Errorf("%w policies", ErrTooMany) 35 ErrTooManyGroups = fmt.Errorf("%w groups", ErrTooMany) 36 ErrNotAllowed = fmt.Errorf("not allowed") 37 ErrAlreadyHasACL = errors.New("already has ACL") 38 ErrAddedActions = errors.New("added actions") 39 ErrEmpty = errors.New("empty") 40 ErrPolicyExists = errors.New("policy exists") 41 ErrHasWarnings = errors.New("has warnings") 42 43 // allPermissions lists all permissions, from most restrictive to 44 // most permissive. It includes "" for some edge cases. 45 allPermissions = []model.ACLPermission{"", acl.ReadPermission, acl.WritePermission, acl.SuperPermission, acl.AdminPermission} 46 ) 47 48 func MigrateToACL(ctx context.Context, kvStore kv.Store, cfg *config.Config, logger logging.Logger, version int, force bool) error { 49 if !cfg.IsAuthUISimplified() { 50 fmt.Println("skipping ACL migration - not simplified") 51 return updateKVSchemaVersion(ctx, kvStore, kv.ACLNoReposMigrateVersion) 52 } 53 54 // handle migrate within ACLs 55 if version == kv.ACLMigrateVersion { 56 if force { 57 return updateKVSchemaVersion(ctx, kvStore, kv.ACLNoReposMigrateVersion) 58 } else { 59 return fmt.Errorf("migrating from previous version of ACL will leave repository level groups until the group is re-edited - please run migrate again with --force flag or contact services@treeverse.io: %w", kv.ErrMigrationVersion) 60 } 61 } 62 63 type Warning struct { 64 GroupID string 65 ACL model.ACL 66 Warn error 67 } 68 var ( 69 groupReports []Warning 70 usersWithPolicies []string 71 ) 72 updateTime := time.Now() 73 authService := auth.NewAuthService( 74 kvStore, 75 crypt.NewSecretStore([]byte(cfg.Auth.Encrypt.SecretKey)), 76 authparams.ServiceCache(cfg.Auth.Cache), 77 logger.WithField("service", "auth_service"), 78 ) 79 usersWithPolicies, err := rbacToACL(ctx, authService, false, updateTime, func(groupID string, acl model.ACL, warn error) { 80 groupReports = append(groupReports, Warning{GroupID: groupID, ACL: acl, Warn: warn}) 81 }) 82 if err != nil { 83 return fmt.Errorf("failed to upgrade RBAC policies to ACLs: %w", err) 84 } 85 86 hasWarnings := false 87 for _, w := range groupReports { 88 fmt.Printf("GROUP %s\n\tACL: %s\n", w.GroupID, reportACL(w.ACL)) 89 if w.Warn != nil { 90 hasWarnings = true 91 var multi *multierror.Error 92 if errors.As(w.Warn, &multi) { 93 multi.ErrorFormat = func(es []error) string { 94 points := make([]string, len(es)) 95 for i, err := range es { 96 points[i] = fmt.Sprintf("* %s", err) 97 } 98 plural := "s" 99 if len(es) == 1 { 100 plural = "" 101 } 102 return fmt.Sprintf( 103 "%d change%s:\n\t%s\n", 104 len(points), plural, strings.Join(points, "\n\t")) 105 } 106 } 107 fmt.Println(w.Warn) 108 } 109 fmt.Println() 110 } 111 for _, username := range usersWithPolicies { 112 fmt.Printf("USER (%s) detaching directly-attached policies\n", username) 113 } 114 115 if hasWarnings && !force { 116 return fmt.Errorf("warnings found. Please fix or run using --force flag: %w", ErrHasWarnings) 117 } 118 119 _, err = rbacToACL(ctx, authService, true, updateTime, func(groupID string, acl model.ACL, warn error) { 120 groupReports = append(groupReports, Warning{GroupID: groupID, ACL: acl, Warn: warn}) 121 }) 122 if err != nil { 123 return fmt.Errorf("failed to upgrade RBAC policies to ACLs: %w", err) 124 } 125 126 return updateKVSchemaVersion(ctx, kvStore, kv.ACLNoReposMigrateVersion) 127 } 128 129 func reportACL(acl model.ACL) string { 130 return string(acl.Permission) + " on [ALL repositories]" 131 } 132 133 // checkPolicyACLName fails if policy name is named as an ACL policy (start 134 // with PolicyPrefix) but is not an ACL policy. 135 func checkPolicyACLName(ctx context.Context, svc auth.Service, name string) error { 136 if !acl.IsPolicyName(name) { 137 return nil 138 } 139 140 _, err := svc.GetGroup(ctx, name) 141 switch { 142 case errors.Is(err, auth.ErrNotFound): 143 return nil 144 case err == nil: 145 return fmt.Errorf("%s: %w", name, ErrPolicyExists) 146 default: 147 return fmt.Errorf("check policy name %s: %w", name, err) 148 } 149 } 150 151 // rbacToACL translates all groups on svc to use ACLs instead of RBAC 152 // policies. It updates svc only if doUpdate. It calls messageFunc to 153 // report increased permissions. 154 // returns a list of users with directly attached policies 155 func rbacToACL(ctx context.Context, svc auth.Service, doUpdate bool, creationTime time.Time, messageFunc func(string, model.ACL, error)) ([]string, error) { 156 mig := NewACLsMigrator(svc, doUpdate) 157 158 groups, _, err := svc.ListGroups(ctx, &model.PaginationParams{Amount: maxGroups + 1}) 159 if err != nil { 160 return nil, fmt.Errorf("list groups: %w", err) 161 } 162 if len(groups) > maxGroups { 163 return nil, fmt.Errorf("%w (got %d)", ErrTooManyGroups, len(groups)) 164 } 165 for _, group := range groups { 166 var warnings error 167 168 policies, _, err := svc.ListGroupPolicies(ctx, group.DisplayName, &model.PaginationParams{Amount: maxGroupPolicies + 1}) 169 if err != nil { 170 return nil, fmt.Errorf("list group %+v policies: %w", group, err) 171 } 172 if len(policies) > maxGroupPolicies { 173 return nil, fmt.Errorf("group %+v: %w (got %d)", group, ErrTooManyPolicies, len(policies)) 174 } 175 newACL, warn, err := mig.NewACLForPolicies(ctx, policies) 176 if err != nil { 177 return nil, fmt.Errorf("create ACL for group %+v: %w", group, err) 178 } 179 if warn != nil { 180 warnings = multierror.Append(warnings, warn) 181 } 182 183 log := logging.FromContext(ctx) 184 log.WithFields(logging.Fields{ 185 "group": group.DisplayName, 186 "acl": fmt.Sprintf("%+v", newACL), 187 }).Info("Computed ACL") 188 189 aclPolicyName := acl.PolicyName(group.DisplayName) 190 err = checkPolicyACLName(ctx, svc, aclPolicyName) 191 if err != nil { 192 warnings = multierror.Append(warnings, warn) 193 } 194 policyExists := errors.Is(err, ErrPolicyExists) 195 if doUpdate { 196 err = acl.WriteGroupACL(ctx, svc, group.DisplayName, *newACL, creationTime, policyExists) 197 if errors.Is(err, auth.ErrAlreadyExists) { 198 warnings = multierror.Append(warnings, err) 199 } else if err != nil { 200 return nil, err 201 } 202 } 203 204 messageFunc(group.DisplayName, *newACL, warnings) 205 } 206 var usersWithPolicies []string 207 hasMoreUser := true 208 afterUser := "" 209 for hasMoreUser { 210 // get membership groups to user 211 users, userPaginator, err := svc.ListUsers(ctx, &model.PaginationParams{ 212 After: afterUser, 213 Amount: pageSize, 214 }) 215 if err != nil { 216 return nil, err 217 } 218 219 for _, user := range users { 220 // get policies attracted to user 221 hasMoreUserPolicy := true 222 afterUserPolicy := "" 223 for hasMoreUserPolicy { 224 userPolicies, userPoliciesPaginator, err := svc.ListUserPolicies(ctx, user.Username, &model.PaginationParams{ 225 After: afterUserPolicy, 226 Amount: pageSize, 227 }) 228 if err != nil { 229 return nil, fmt.Errorf("list user policies: %w", err) 230 } 231 if len(userPolicies) > 0 { 232 usersWithPolicies = append(usersWithPolicies, user.Username) 233 } 234 if !doUpdate { 235 break 236 } 237 for _, policy := range userPolicies { 238 if err := svc.DetachPolicyFromUser(ctx, policy.DisplayName, user.Username); err != nil { 239 return nil, fmt.Errorf("failed detaching policy %s from user %s: %w", policy.DisplayName, user.Username, err) 240 } 241 } 242 afterUserPolicy = userPoliciesPaginator.NextPageToken 243 hasMoreUserPolicy = userPoliciesPaginator.NextPageToken != "" 244 } 245 } 246 afterUser = userPaginator.NextPageToken 247 hasMoreUser = userPaginator.NextPageToken != "" 248 } 249 return usersWithPolicies, nil 250 } 251 252 // ACLsMigrator migrates from policies to ACLs. 253 type ACLsMigrator struct { 254 svc auth.Service 255 doUpdate bool 256 257 Actions map[model.ACLPermission]map[string]struct{} 258 } 259 260 func makeSet(allEls ...[]string) map[string]struct{} { 261 ret := make(map[string]struct{}) 262 for _, els := range allEls { 263 for _, el := range els { 264 ret[el] = struct{}{} 265 } 266 } 267 return ret 268 } 269 270 // NewACLsMigrator returns an ACLsMigrator. That ACLsMigrator will only 271 // check (change nothing) if doUpdate is false. 272 func NewACLsMigrator(svc auth.Service, doUpdate bool) *ACLsMigrator { 273 manageOwnCredentials := auth.GetActionsForPolicyTypeOrDie("AuthManageOwnCredentials") 274 ciRead := auth.GetActionsForPolicyTypeOrDie("RepoManagementRead") 275 return &ACLsMigrator{ 276 svc: svc, 277 doUpdate: doUpdate, 278 Actions: map[model.ACLPermission]map[string]struct{}{ 279 acl.AdminPermission: makeSet(auth.GetActionsForPolicyTypeOrDie("AllAccess")), 280 acl.SuperPermission: makeSet(auth.GetActionsForPolicyTypeOrDie("FSFullAccess"), manageOwnCredentials, ciRead), 281 acl.WritePermission: makeSet(auth.GetActionsForPolicyTypeOrDie("FSReadWrite"), manageOwnCredentials, ciRead), 282 acl.ReadPermission: makeSet(auth.GetActionsForPolicyTypeOrDie("FSRead"), manageOwnCredentials), 283 }, 284 } 285 } 286 287 // NewACLForPolicies converts policies of group name to an ACL. warn 288 // summarizes all losses in converting policies to ACL. err holds an error 289 // if conversion failed. 290 func (mig *ACLsMigrator) NewACLForPolicies(ctx context.Context, policies []*model.Policy) (acl *model.ACL, warn error, err error) { 291 warn = nil 292 acl = new(model.ACL) 293 294 allAllowedActions := make(map[string]struct{}) 295 for _, p := range policies { 296 if p.ACL.Permission != "" { 297 warn = multierror.Append(warn, fmt.Errorf("policy %s: %w", p.DisplayName, ErrAlreadyHasACL)) 298 } 299 300 for _, s := range p.Statement { 301 if s.Effect != model.StatementEffectAllow { 302 warn = multierror.Append(warn, fmt.Errorf("ignore policy %s statement %+v: %w", p.DisplayName, s, ErrNotAllowed)) 303 } 304 sp, err := mig.ComputePermission(ctx, s.Action) 305 if err != nil { 306 return nil, warn, fmt.Errorf("convert policy %s statement %+v: %w", p.DisplayName, s, err) 307 } 308 for _, allowedAction := range expandMatchingActions(s.Action) { 309 allAllowedActions[allowedAction] = struct{}{} 310 } 311 312 if BroaderPermission(sp, acl.Permission) { 313 acl.Permission = sp 314 } 315 } 316 } 317 addedActions := mig.ComputeAddedActions(acl.Permission, allAllowedActions) 318 if len(addedActions) > 0 { 319 warn = multierror.Append(warn, fmt.Errorf("%w: %s", ErrAddedActions, strings.Join(addedActions, ", "))) 320 } 321 return acl, warn, err 322 } 323 324 func expandMatchingActions(patterns []string) []string { 325 ret := make([]string, 0, len(patterns)) 326 for _, action := range permissions.Actions { 327 for _, pattern := range patterns { 328 if wildcard.Match(pattern, action) { 329 ret = append(ret, action) 330 } 331 } 332 } 333 return ret 334 } 335 336 func someActionMatches(action string, availableActions map[string]struct{}) bool { 337 for availableAction := range availableActions { 338 if wildcard.Match(availableAction, action) { 339 return true 340 } 341 } 342 return false 343 } 344 345 func (mig *ACLsMigrator) GetMinPermission(action string) model.ACLPermission { 346 if !strings.ContainsAny(action, "*?") { 347 for _, permission := range allPermissions { 348 if someActionMatches(action, mig.Actions[permission]) { 349 return permission 350 } 351 } 352 return "" 353 } 354 355 // Try a wildcard match against all known actions: find the least 356 // permission that allows all actions that the action pattern 357 // matches. 358 for _, permission := range allPermissions { 359 // This loop is reasonably efficient only for small numbers 360 // of ACL permissions. 361 actionsForPermission := mig.Actions[permission] 362 permissionOK := true 363 for _, a := range permissions.Actions { 364 if !wildcard.Match(action, a) { 365 // 'a' does not include action. 366 continue 367 } 368 if someActionMatches(a, actionsForPermission) { 369 // 'a' is allowed at permission. 370 continue 371 } 372 permissionOK = false 373 break 374 } 375 if permissionOK { 376 return permission 377 } 378 } 379 panic(fmt.Sprintf("Unknown action %s", action)) 380 } 381 382 // ComputePermission returns ACL permission for actions and the actions that 383 // applying that permission will add to it. 384 func (mig *ACLsMigrator) ComputePermission(ctx context.Context, actions []string) (model.ACLPermission, error) { 385 log := logging.FromContext(ctx) 386 permission := model.ACLPermission("") 387 for _, action := range actions { 388 p := mig.GetMinPermission(action) 389 if BroaderPermission(p, permission) { 390 log.WithFields(logging.Fields{ 391 "action": action, 392 "permission": p, 393 "prev_permission": permission, 394 }).Debug("Increase permission") 395 permission = p 396 } else { 397 log.WithFields(logging.Fields{ 398 "action": action, 399 "permission": p, 400 }).Trace("Permission") 401 } 402 } 403 if permission == "" { 404 return "", fmt.Errorf("%w actions", ErrEmpty) 405 } 406 407 return permission, nil 408 } 409 410 // ComputeAddedActions returns the list of actions that permission allows 411 // that are not in alreadyAllowedActions. 412 func (mig *ACLsMigrator) ComputeAddedActions(permission model.ACLPermission, alreadyAllowedActions map[string]struct{}) []string { 413 var allAllowedActions map[string]struct{} 414 switch permission { 415 case acl.ReadPermission: 416 allAllowedActions = mig.Actions[acl.ReadPermission] 417 case acl.WritePermission: 418 allAllowedActions = mig.Actions[acl.WritePermission] 419 case acl.SuperPermission: 420 allAllowedActions = mig.Actions[acl.SuperPermission] 421 case acl.AdminPermission: 422 default: 423 allAllowedActions = mig.Actions[acl.AdminPermission] 424 } 425 addedActions := make(map[string]struct{}, len(allAllowedActions)) 426 for _, action := range permissions.Actions { 427 if someActionMatches(action, allAllowedActions) && !someActionMatches(action, alreadyAllowedActions) { 428 addedActions[action] = struct{}{} 429 } 430 } 431 addedActionsSlice := make([]string, 0, len(addedActions)) 432 for action := range addedActions { 433 addedActionsSlice = append(addedActionsSlice, action) 434 } 435 return addedActionsSlice 436 } 437 438 // BroaderPermission returns true if a offers strictly more permissions than b. Unknown ACLPermission will panic. 439 func BroaderPermission(a, b model.ACLPermission) bool { 440 switch a { 441 case "": 442 return false 443 case acl.ReadPermission: 444 return b == "" 445 case acl.WritePermission: 446 return b == "" || b == acl.ReadPermission 447 case acl.SuperPermission: 448 return b == "" || b == acl.ReadPermission || b == acl.WritePermission 449 case acl.AdminPermission: 450 return b == "" || b == acl.ReadPermission || b == acl.WritePermission || b == acl.SuperPermission 451 } 452 panic(fmt.Sprintf("impossible comparison %s and %s", a, b)) 453 }