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  }