go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/analysis/rpc/rules.go (about)

     1  // Copyright 2022 The LUCI Authors.
     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  package rpc
    16  
    17  import (
    18  	"context"
    19  	"fmt"
    20  	"regexp"
    21  	"time"
    22  
    23  	"google.golang.org/grpc/codes"
    24  	"google.golang.org/grpc/status"
    25  	"google.golang.org/protobuf/types/known/timestamppb"
    26  
    27  	"go.chromium.org/luci/common/errors"
    28  	"go.chromium.org/luci/common/logging"
    29  	"go.chromium.org/luci/grpc/appstatus"
    30  	"go.chromium.org/luci/server/auth"
    31  	"go.chromium.org/luci/server/span"
    32  
    33  	"go.chromium.org/luci/analysis/internal/bugs"
    34  	bugspb "go.chromium.org/luci/analysis/internal/bugs/proto"
    35  	"go.chromium.org/luci/analysis/internal/clustering"
    36  	"go.chromium.org/luci/analysis/internal/clustering/rules"
    37  	"go.chromium.org/luci/analysis/internal/config/compiledcfg"
    38  	"go.chromium.org/luci/analysis/internal/perms"
    39  	configpb "go.chromium.org/luci/analysis/proto/config"
    40  	pb "go.chromium.org/luci/analysis/proto/v1"
    41  )
    42  
    43  // Rules implements pb.RulesServer.
    44  type rulesServer struct {
    45  }
    46  
    47  // NewRulesSever returns a new pb.RulesServer.
    48  func NewRulesSever() pb.RulesServer {
    49  	return &pb.DecoratedRules{
    50  		Prelude:  checkAllowedPrelude,
    51  		Service:  &rulesServer{},
    52  		Postlude: gRPCifyAndLogPostlude,
    53  	}
    54  }
    55  
    56  // Retrieves a rule.
    57  func (*rulesServer) Get(ctx context.Context, req *pb.GetRuleRequest) (*pb.Rule, error) {
    58  	project, ruleID, err := parseRuleName(req.Name)
    59  	if err != nil {
    60  		return nil, invalidArgumentError(err)
    61  	}
    62  	if err := perms.VerifyProjectPermissions(ctx, project, perms.PermGetRule); err != nil {
    63  		return nil, err
    64  	}
    65  	ruleMask, err := ruleFieldAccess(ctx, project)
    66  	if err != nil {
    67  		return nil, err
    68  	}
    69  
    70  	cfg, err := readProjectConfig(ctx, project)
    71  	if err != nil {
    72  		return nil, err
    73  	}
    74  
    75  	r, err := rules.Read(span.Single(ctx), project, ruleID)
    76  	if err != nil {
    77  		if err == rules.NotExistsErr {
    78  			return nil, appstatus.Error(codes.NotFound, "rule does not exist")
    79  		}
    80  		// This will result in an internal error being reported to the caller.
    81  		return nil, errors.Annotate(err, "reading rule %s", ruleID).Err()
    82  	}
    83  	return createRulePB(r, cfg.Config, ruleMask), nil
    84  }
    85  
    86  // ruleMask captures the fields the caller has access to see.
    87  type ruleMask struct {
    88  	// Include the definition of the rule.
    89  	// Guarded by analysis.rules.getDefinition permission.
    90  	IncludeDefinition bool
    91  	// Include the email address of users who created/modified the rule.
    92  	// Limited to Googlers only.
    93  	IncludeAuditUsers bool
    94  }
    95  
    96  // ruleFieldAccess checks the caller's access to rule fields in the given
    97  // project and returns a ruleMask corresponding to the fields they are
    98  // allowed to see.
    99  func ruleFieldAccess(ctx context.Context, project string) (ruleMask, error) {
   100  	var result ruleMask
   101  	var err error
   102  	result.IncludeDefinition, err = perms.HasProjectPermission(ctx, project, perms.PermGetRuleDefinition)
   103  	if err != nil {
   104  		return ruleMask{}, errors.Annotate(err, "determining access to rule definition").Err()
   105  	}
   106  	result.IncludeAuditUsers, err = auth.IsMember(ctx, auditUsersAccessGroup)
   107  	if err != nil {
   108  		return ruleMask{}, errors.Annotate(err, "determining access to read created/last modified users").Err()
   109  	}
   110  	return result, nil
   111  }
   112  
   113  // Lists rules.
   114  func (*rulesServer) List(ctx context.Context, req *pb.ListRulesRequest) (*pb.ListRulesResponse, error) {
   115  	project, err := parseProjectName(req.Parent)
   116  	if err != nil {
   117  		return nil, invalidArgumentError(err)
   118  	}
   119  	if err := perms.VerifyProjectPermissions(ctx, project, perms.PermListRules); err != nil {
   120  		return nil, err
   121  	}
   122  	ruleMask, err := ruleFieldAccess(ctx, project)
   123  	if err != nil {
   124  		return nil, err
   125  	}
   126  
   127  	cfg, err := readProjectConfig(ctx, project)
   128  	if err != nil {
   129  		return nil, err
   130  	}
   131  
   132  	// TODO: Update to read all rules (not just active), and implement pagination.
   133  	rs, err := rules.ReadActive(span.Single(ctx), project)
   134  	if err != nil {
   135  		// GRPCifyAndLog will log this, and report an internal error.
   136  		return nil, errors.Annotate(err, "reading rules").Err()
   137  	}
   138  
   139  	rpbs := make([]*pb.Rule, 0, len(rs))
   140  	for _, r := range rs {
   141  		rpbs = append(rpbs, createRulePB(r, cfg.Config, ruleMask))
   142  	}
   143  	response := &pb.ListRulesResponse{
   144  		Rules: rpbs,
   145  	}
   146  	return response, nil
   147  }
   148  
   149  // Creates a new rule.
   150  func (*rulesServer) Create(ctx context.Context, req *pb.CreateRuleRequest) (*pb.Rule, error) {
   151  	project, err := parseProjectName(req.Parent)
   152  	if err != nil {
   153  		return nil, invalidArgumentError(err)
   154  	}
   155  	if err := perms.VerifyProjectPermissions(ctx, project, perms.PermCreateRule, perms.PermGetRuleDefinition); err != nil {
   156  		return nil, err
   157  	}
   158  	ruleMask, err := ruleFieldAccess(ctx, project)
   159  	if err != nil {
   160  		return nil, err
   161  	}
   162  
   163  	cfg, err := readProjectConfig(ctx, project)
   164  	if err != nil {
   165  		return nil, err
   166  	}
   167  
   168  	ruleID, err := rules.GenerateID()
   169  	if err != nil {
   170  		return nil, errors.Annotate(err, "generating Rule ID").Err()
   171  	}
   172  	user := auth.CurrentUser(ctx).Email
   173  
   174  	r := &rules.Entry{
   175  		Project:        project,
   176  		RuleID:         ruleID,
   177  		RuleDefinition: req.Rule.GetRuleDefinition(),
   178  		BugID: bugs.BugID{
   179  			System: req.Rule.Bug.GetSystem(),
   180  			ID:     req.Rule.Bug.GetId(),
   181  		},
   182  		IsActive:              req.Rule.GetIsActive(),
   183  		IsManagingBug:         req.Rule.GetIsManagingBug(),
   184  		IsManagingBugPriority: req.Rule.GetIsManagingBugPriority(),
   185  		SourceCluster: clustering.ClusterID{
   186  			Algorithm: req.Rule.SourceCluster.GetAlgorithm(),
   187  			ID:        req.Rule.SourceCluster.GetId(),
   188  		},
   189  		BugManagementState: &bugspb.BugManagementState{},
   190  	}
   191  
   192  	if err := validateBugAgainstConfig(cfg, r.BugID); err != nil {
   193  		return nil, invalidArgumentError(err)
   194  	}
   195  
   196  	commitTime, err := span.ReadWriteTransaction(ctx, func(ctx context.Context) error {
   197  		// Verify the bug is not used by another rule in this project.
   198  		bugRules, err := rules.ReadByBug(ctx, r.BugID)
   199  		if err != nil {
   200  			return err
   201  		}
   202  		for _, otherRule := range bugRules {
   203  			if otherRule.IsManagingBug {
   204  				// Avoid conflicts by silently making the bug not managed
   205  				// by this rule if there is another rule managing it.
   206  				// Note: this validation implicitly discloses the existence
   207  				// of rules in projects other than those the user may have
   208  				// access to.
   209  				r.IsManagingBug = false
   210  			}
   211  			if otherRule.Project == r.Project {
   212  				return invalidArgumentError(fmt.Errorf("bug already used by a rule in the same project (%s/%s)", otherRule.Project, otherRule.RuleID))
   213  			}
   214  		}
   215  
   216  		ms, err := rules.Create(r, user)
   217  		if err != nil {
   218  			return invalidArgumentError(err)
   219  		}
   220  		span.BufferWrite(ctx, ms)
   221  		return nil
   222  	})
   223  	if err != nil {
   224  		return nil, err
   225  	}
   226  	r.CreateTime = commitTime.In(time.UTC)
   227  	r.CreateUser = user
   228  	r.LastAuditableUpdateTime = commitTime.In(time.UTC)
   229  	r.LastAuditableUpdateUser = user
   230  	r.LastUpdateTime = commitTime.In(time.UTC)
   231  	r.PredicateLastUpdateTime = commitTime.In(time.UTC)
   232  	r.IsManagingBugPriorityLastUpdateTime = commitTime.In(time.UTC)
   233  
   234  	// Log rule changes to provide a way of recovering old system state
   235  	// if malicious or unintended updates occur.
   236  	logRuleCreate(ctx, r)
   237  
   238  	return createRulePB(r, cfg.Config, ruleMask), nil
   239  }
   240  
   241  func logRuleCreate(ctx context.Context, rule *rules.Entry) {
   242  	logging.Infof(ctx, "Rule created (%s/%s): %s", rule.Project, rule.RuleID, formatRule(rule))
   243  }
   244  
   245  // Updates a rule.
   246  func (*rulesServer) Update(ctx context.Context, req *pb.UpdateRuleRequest) (*pb.Rule, error) {
   247  	project, ruleID, err := parseRuleName(req.Rule.GetName())
   248  	if err != nil {
   249  		return nil, invalidArgumentError(err)
   250  	}
   251  	if err := perms.VerifyProjectPermissions(ctx, project, perms.PermUpdateRule, perms.PermGetRuleDefinition); err != nil {
   252  		return nil, err
   253  	}
   254  	ruleMask, err := ruleFieldAccess(ctx, project)
   255  	if err != nil {
   256  		return nil, err
   257  	}
   258  
   259  	cfg, err := readProjectConfig(ctx, project)
   260  	if err != nil {
   261  		return nil, err
   262  	}
   263  
   264  	user := auth.CurrentUser(ctx).Email
   265  
   266  	var predicateUpdated bool
   267  	var managingBugPriorityUpdated bool
   268  	var originalRule *rules.Entry
   269  	var updatedRule *rules.Entry
   270  	f := func(ctx context.Context) error {
   271  		rule, err := rules.Read(ctx, project, ruleID)
   272  		if err != nil {
   273  			if err == rules.NotExistsErr {
   274  				return appstatus.Error(codes.NotFound, "rule does not exist")
   275  			}
   276  			// This will result in an internal error being reported to the
   277  			// caller.
   278  			return errors.Annotate(err, "read rule").Err()
   279  		}
   280  		originalRule = &rules.Entry{}
   281  		*originalRule = *rule
   282  
   283  		if req.Etag != "" && !isETagMatching(rule, req.Etag) {
   284  			// Attach a codes.Aborted appstatus to a vanilla error to avoid
   285  			// ReadWriteTransaction interpreting this case for a scenario
   286  			// in which it should retry the transaction.
   287  			err := errors.New("etag mismatch")
   288  			return appstatus.Attach(err, status.New(codes.Aborted, "the rule was modified since it was last read; the update was not applied."))
   289  		}
   290  
   291  		// If we are updating the RuleDefinition or IsActive field.
   292  		updatePredicate := false
   293  
   294  		// If we are updating the Bug field
   295  		updatingBug := false
   296  
   297  		// If we are updating the IsManagingBug field.
   298  		updatingManaged := false
   299  
   300  		// If we are updating the IsManagingBugPriority field.
   301  		updatingIsManagingBugPriority := false
   302  
   303  		// Tracks if the caller explicitly requested .IsManagingBug = true, even
   304  		// if this is a no-op.
   305  		requestedManagedTrue := false
   306  
   307  		for _, path := range req.UpdateMask.Paths {
   308  			// Only limited fields may be modified by the client.
   309  			switch path {
   310  			case "rule_definition":
   311  				if rule.RuleDefinition != req.Rule.RuleDefinition {
   312  					rule.RuleDefinition = req.Rule.RuleDefinition
   313  					updatePredicate = true
   314  				}
   315  			case "bug":
   316  				bugID := bugs.BugID{
   317  					System: req.Rule.Bug.GetSystem(),
   318  					ID:     req.Rule.Bug.GetId(),
   319  				}
   320  				if err := validateBugAgainstConfig(cfg, bugID); err != nil {
   321  					return invalidArgumentError(err)
   322  				}
   323  				if rule.BugID != bugID {
   324  					updatingBug = true // Triggers validation.
   325  					rule.BugID = bugID
   326  
   327  					// Changing the associated bug requires us to reset flags
   328  					// tracking notifications sent to the associated bug.
   329  					rule.BugManagementState.RuleAssociationNotified = false
   330  					if rule.BugManagementState.PolicyState != nil {
   331  						for _, policyState := range rule.BugManagementState.PolicyState {
   332  							policyState.ActivationNotified = false
   333  						}
   334  					}
   335  				}
   336  			case "is_active":
   337  				if rule.IsActive != req.Rule.IsActive {
   338  					rule.IsActive = req.Rule.IsActive
   339  					updatePredicate = true
   340  				}
   341  			case "is_managing_bug":
   342  				if req.Rule.IsManagingBug {
   343  					requestedManagedTrue = true
   344  				}
   345  				if rule.IsManagingBug != req.Rule.IsManagingBug {
   346  					updatingManaged = true // Triggers validation.
   347  					rule.IsManagingBug = req.Rule.IsManagingBug
   348  				}
   349  			case "is_managing_bug_priority":
   350  				if rule.IsManagingBugPriority != req.Rule.IsManagingBugPriority {
   351  					updatingIsManagingBugPriority = true
   352  					rule.IsManagingBugPriority = req.Rule.IsManagingBugPriority
   353  				}
   354  			default:
   355  				return invalidArgumentError(fmt.Errorf("unsupported field mask: %s", path))
   356  			}
   357  		}
   358  
   359  		if updatingBug || updatingManaged {
   360  			// Verify the new bug is not used by another rule in the
   361  			// same project, and that there are not multiple rules
   362  			// managing the same bug.
   363  			bugRules, err := rules.ReadByBug(ctx, rule.BugID)
   364  			if err != nil {
   365  				// This will result in an internal error being reported
   366  				// to the caller.
   367  				return err
   368  			}
   369  			for _, otherRule := range bugRules {
   370  				if otherRule.Project == project && otherRule.RuleID != ruleID {
   371  					return invalidArgumentError(fmt.Errorf("bug already used by a rule in the same project (%s/%s)", otherRule.Project, otherRule.RuleID))
   372  				}
   373  			}
   374  			for _, otherRule := range bugRules {
   375  				if otherRule.Project != project && otherRule.IsManagingBug {
   376  					if requestedManagedTrue {
   377  						// The caller explicitly requested an update of
   378  						// IsManagingBug to true, but we cannot do this.
   379  						return invalidArgumentError(fmt.Errorf("bug already managed by a rule in another project (%s/%s)", otherRule.Project, otherRule.RuleID))
   380  					}
   381  					// If only changing the bug, avoid conflicts by silently
   382  					// making the bug not managed by this rule if there is
   383  					// another rule managing it.
   384  					// Note: this step implicitly discloses the existence
   385  					// of rules in projects other than those the user may have
   386  					// access to.
   387  					rule.IsManagingBug = false
   388  				}
   389  			}
   390  		}
   391  
   392  		ms, err := rules.Update(rule, rules.UpdateOptions{
   393  			IsAuditableUpdate:            true,
   394  			PredicateUpdated:             updatePredicate,
   395  			IsManagingBugPriorityUpdated: updatingIsManagingBugPriority,
   396  		}, user)
   397  		if err != nil {
   398  			return invalidArgumentError(err)
   399  		}
   400  		span.BufferWrite(ctx, ms)
   401  		updatedRule = rule
   402  		predicateUpdated = updatePredicate
   403  		managingBugPriorityUpdated = updatingIsManagingBugPriority
   404  		return nil
   405  	}
   406  	commitTime, err := span.ReadWriteTransaction(ctx, f)
   407  	if err != nil {
   408  		return nil, err
   409  	}
   410  	updatedRule.LastAuditableUpdateTime = commitTime.In(time.UTC)
   411  	updatedRule.LastAuditableUpdateUser = user
   412  	updatedRule.LastUpdateTime = commitTime.In(time.UTC)
   413  	if predicateUpdated {
   414  		updatedRule.PredicateLastUpdateTime = commitTime.In(time.UTC)
   415  	}
   416  	if managingBugPriorityUpdated {
   417  		updatedRule.IsManagingBugPriorityLastUpdateTime = commitTime.In(time.UTC)
   418  	}
   419  	// Log rule changes to provide a way of recovering old system state
   420  	// if malicious or unintended updates occur.
   421  	logRuleUpdate(ctx, originalRule, updatedRule)
   422  
   423  	return createRulePB(updatedRule, cfg.Config, ruleMask), nil
   424  }
   425  
   426  func logRuleUpdate(ctx context.Context, old *rules.Entry, new *rules.Entry) {
   427  	logging.Infof(ctx, "Rule updated (%s/%s): from %s to %s", old.Project, old.RuleID, formatRule(old), formatRule(new))
   428  }
   429  
   430  func formatRule(r *rules.Entry) string {
   431  	return fmt.Sprintf("{\n"+
   432  		"\tRuleDefinition: %q,\n"+
   433  		"\tBugID: %q,\n"+
   434  		"\tIsActive: %v,\n"+
   435  		"\tIsManagingBug: %v,\n"+
   436  		"\tIsManagingBugPriority: %v,\n"+
   437  		"\tSourceCluster: %q\n"+
   438  		"\tLastAuditableUpdate: %q\n"+
   439  		"\tLastUpdated: %q\n"+
   440  		"}", r.RuleDefinition, r.BugID, r.IsActive, r.IsManagingBug,
   441  		r.IsManagingBugPriority, r.SourceCluster,
   442  		r.LastAuditableUpdateTime.Format(time.RFC3339Nano),
   443  		r.LastUpdateTime.Format(time.RFC3339Nano))
   444  }
   445  
   446  // LookupBug looks up the rule associated with the given bug.
   447  func (*rulesServer) LookupBug(ctx context.Context, req *pb.LookupBugRequest) (*pb.LookupBugResponse, error) {
   448  	bug := bugs.BugID{
   449  		System: req.System,
   450  		ID:     req.Id,
   451  	}
   452  	if err := bug.Validate(); err != nil {
   453  		return nil, invalidArgumentError(err)
   454  	}
   455  	rules, err := rules.ReadByBug(span.Single(ctx), bug)
   456  	if err != nil {
   457  		// This will result in an internal error being reported to the caller.
   458  		return nil, errors.Annotate(err, "reading rule by bug %s:%s", bug.System, bug.ID).Err()
   459  	}
   460  	ruleNames := make([]string, 0, len(rules))
   461  	for _, rule := range rules {
   462  		allowed, err := perms.HasProjectPermission(ctx, rule.Project, perms.PermListRules)
   463  		if err != nil {
   464  			return nil, err
   465  		}
   466  		if allowed {
   467  			ruleNames = append(ruleNames, ruleName(rule.Project, rule.RuleID))
   468  		}
   469  	}
   470  	return &pb.LookupBugResponse{
   471  		Rules: ruleNames,
   472  	}, nil
   473  }
   474  
   475  func createRulePB(r *rules.Entry, cfg *configpb.ProjectConfig, mask ruleMask) *pb.Rule {
   476  	definition := ""
   477  	if mask.IncludeDefinition {
   478  		definition = r.RuleDefinition
   479  	}
   480  	creationUser := ""
   481  	lastAuditableUpdateUser := ""
   482  	if mask.IncludeAuditUsers {
   483  		creationUser = r.CreateUser
   484  		lastAuditableUpdateUser = r.LastAuditableUpdateUser
   485  	}
   486  	return &pb.Rule{
   487  		Name:                                ruleName(r.Project, r.RuleID),
   488  		Project:                             r.Project,
   489  		RuleId:                              r.RuleID,
   490  		RuleDefinition:                      definition,
   491  		Bug:                                 createAssociatedBugPB(r.BugID, cfg),
   492  		IsActive:                            r.IsActive,
   493  		IsManagingBug:                       r.IsManagingBug,
   494  		IsManagingBugPriority:               r.IsManagingBugPriority,
   495  		IsManagingBugPriorityLastUpdateTime: timestamppb.New(r.IsManagingBugPriorityLastUpdateTime),
   496  		SourceCluster: &pb.ClusterId{
   497  			Algorithm: r.SourceCluster.Algorithm,
   498  			Id:        r.SourceCluster.ID,
   499  		},
   500  		BugManagementState:      rules.ToExternalBugManagementStatePB(r.BugManagementState),
   501  		CreateTime:              timestamppb.New(r.CreateTime),
   502  		CreateUser:              creationUser,
   503  		LastAuditableUpdateTime: timestamppb.New(r.LastAuditableUpdateTime),
   504  		LastAuditableUpdateUser: lastAuditableUpdateUser,
   505  		LastUpdateTime:          timestamppb.New(r.LastUpdateTime),
   506  		PredicateLastUpdateTime: timestamppb.New(r.PredicateLastUpdateTime),
   507  		Etag:                    ruleETag(r, mask),
   508  	}
   509  }
   510  
   511  // ruleETag returns the HTTP ETag for the given rule.
   512  func ruleETag(rule *rules.Entry, mask ruleMask) string {
   513  	definitionFilter := ""
   514  	if mask.IncludeDefinition {
   515  		definitionFilter = "+d"
   516  	}
   517  	userFilter := ""
   518  	if mask.IncludeAuditUsers {
   519  		userFilter = "+u"
   520  	}
   521  	// Encode whether the definition and user were included,
   522  	// so that if the user is granted access to either of
   523  	// these fields, the browser cache is invalidated.
   524  	return fmt.Sprintf(`W/"%s%s/%s"`, definitionFilter, userFilter, rule.LastUpdateTime.UTC().Format(time.RFC3339Nano))
   525  }
   526  
   527  // etagRegexp extracts the rule last modified timestamp from a rule ETag.
   528  var etagRegexp = regexp.MustCompile(`^W/"(?:\+[ud])*/(.*)"$`)
   529  
   530  // isETagMatching determines if the Etag is consistent with the specified
   531  // Rule version.
   532  func isETagMatching(rule *rules.Entry, etag string) bool {
   533  	m := etagRegexp.FindStringSubmatch(etag)
   534  	if len(m) < 2 {
   535  		return false
   536  	}
   537  	return m[1] == rule.LastUpdateTime.UTC().Format(time.RFC3339Nano)
   538  }
   539  
   540  // validateBugAgainstConfig validates the specified bug is consistent with
   541  // the project configuration.
   542  func validateBugAgainstConfig(cfg *compiledcfg.ProjectConfig, bug bugs.BugID) error {
   543  	switch bug.System {
   544  	case bugs.MonorailSystem:
   545  		project, _, err := bug.MonorailProjectAndID()
   546  		if err != nil {
   547  			return err
   548  		}
   549  		monorailCfg := cfg.Config.BugManagement.GetMonorail()
   550  		if monorailCfg == nil {
   551  			return fmt.Errorf("monorail bug system not enabled for this LUCI project")
   552  		}
   553  		if project != monorailCfg.Project {
   554  			return fmt.Errorf("bug not in expected monorail project (%s)", monorailCfg.Project)
   555  		}
   556  	case bugs.BuganizerSystem:
   557  		// Buganizer bugs are permitted for all LUCI Analysis projects.
   558  	default:
   559  		return fmt.Errorf("unsupported bug system: %s", bug.System)
   560  	}
   561  	return nil
   562  }