You can not select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
				
					
					
						
							380 lines
						
					
					
						
							10 KiB
						
					
					
				
			
		
		
		
			
			
			
		
		
	
	
							380 lines
						
					
					
						
							10 KiB
						
					
					
				| package policy | |
| 
 | |
| import ( | |
| 	"context" | |
| 	"encoding/json" | |
| 	"fmt" | |
| 	"strings" | |
| 	"sync" | |
| 	"time" | |
| 
 | |
| 	"github.com/seaweedfs/seaweedfs/weed/glog" | |
| 	"github.com/seaweedfs/seaweedfs/weed/pb" | |
| 	"github.com/seaweedfs/seaweedfs/weed/pb/filer_pb" | |
| 	"google.golang.org/grpc" | |
| ) | |
| 
 | |
| // MemoryPolicyStore implements PolicyStore using in-memory storage | |
| type MemoryPolicyStore struct { | |
| 	policies map[string]*PolicyDocument | |
| 	mutex    sync.RWMutex | |
| } | |
| 
 | |
| // NewMemoryPolicyStore creates a new memory-based policy store | |
| func NewMemoryPolicyStore() *MemoryPolicyStore { | |
| 	return &MemoryPolicyStore{ | |
| 		policies: make(map[string]*PolicyDocument), | |
| 	} | |
| } | |
| 
 | |
| // StorePolicy stores a policy document in memory | |
| func (s *MemoryPolicyStore) StorePolicy(ctx context.Context, name string, policy *PolicyDocument) error { | |
| 	if name == "" { | |
| 		return fmt.Errorf("policy name cannot be empty") | |
| 	} | |
| 
 | |
| 	if policy == nil { | |
| 		return fmt.Errorf("policy cannot be nil") | |
| 	} | |
| 
 | |
| 	s.mutex.Lock() | |
| 	defer s.mutex.Unlock() | |
| 
 | |
| 	// Deep copy the policy to prevent external modifications | |
| 	s.policies[name] = copyPolicyDocument(policy) | |
| 	return nil | |
| } | |
| 
 | |
| // GetPolicy retrieves a policy document from memory | |
| func (s *MemoryPolicyStore) GetPolicy(ctx context.Context, name string) (*PolicyDocument, error) { | |
| 	if name == "" { | |
| 		return nil, fmt.Errorf("policy name cannot be empty") | |
| 	} | |
| 
 | |
| 	s.mutex.RLock() | |
| 	defer s.mutex.RUnlock() | |
| 
 | |
| 	policy, exists := s.policies[name] | |
| 	if !exists { | |
| 		return nil, fmt.Errorf("policy not found: %s", name) | |
| 	} | |
| 
 | |
| 	// Return a copy to prevent external modifications | |
| 	return copyPolicyDocument(policy), nil | |
| } | |
| 
 | |
| // DeletePolicy deletes a policy document from memory | |
| func (s *MemoryPolicyStore) DeletePolicy(ctx context.Context, name string) error { | |
| 	if name == "" { | |
| 		return fmt.Errorf("policy name cannot be empty") | |
| 	} | |
| 
 | |
| 	s.mutex.Lock() | |
| 	defer s.mutex.Unlock() | |
| 
 | |
| 	delete(s.policies, name) | |
| 	return nil | |
| } | |
| 
 | |
| // ListPolicies lists all policy names in memory | |
| func (s *MemoryPolicyStore) ListPolicies(ctx context.Context) ([]string, error) { | |
| 	s.mutex.RLock() | |
| 	defer s.mutex.RUnlock() | |
| 
 | |
| 	names := make([]string, 0, len(s.policies)) | |
| 	for name := range s.policies { | |
| 		names = append(names, name) | |
| 	} | |
| 
 | |
| 	return names, nil | |
| } | |
| 
 | |
| // copyPolicyDocument creates a deep copy of a policy document | |
| func copyPolicyDocument(original *PolicyDocument) *PolicyDocument { | |
| 	if original == nil { | |
| 		return nil | |
| 	} | |
| 
 | |
| 	copied := &PolicyDocument{ | |
| 		Version: original.Version, | |
| 		Id:      original.Id, | |
| 	} | |
| 
 | |
| 	// Copy statements | |
| 	copied.Statement = make([]Statement, len(original.Statement)) | |
| 	for i, stmt := range original.Statement { | |
| 		copied.Statement[i] = Statement{ | |
| 			Sid:          stmt.Sid, | |
| 			Effect:       stmt.Effect, | |
| 			Principal:    stmt.Principal, | |
| 			NotPrincipal: stmt.NotPrincipal, | |
| 		} | |
| 
 | |
| 		// Copy action slice | |
| 		if stmt.Action != nil { | |
| 			copied.Statement[i].Action = make([]string, len(stmt.Action)) | |
| 			copy(copied.Statement[i].Action, stmt.Action) | |
| 		} | |
| 
 | |
| 		// Copy NotAction slice | |
| 		if stmt.NotAction != nil { | |
| 			copied.Statement[i].NotAction = make([]string, len(stmt.NotAction)) | |
| 			copy(copied.Statement[i].NotAction, stmt.NotAction) | |
| 		} | |
| 
 | |
| 		// Copy resource slice | |
| 		if stmt.Resource != nil { | |
| 			copied.Statement[i].Resource = make([]string, len(stmt.Resource)) | |
| 			copy(copied.Statement[i].Resource, stmt.Resource) | |
| 		} | |
| 
 | |
| 		// Copy NotResource slice | |
| 		if stmt.NotResource != nil { | |
| 			copied.Statement[i].NotResource = make([]string, len(stmt.NotResource)) | |
| 			copy(copied.Statement[i].NotResource, stmt.NotResource) | |
| 		} | |
| 
 | |
| 		// Copy condition map (shallow copy for now) | |
| 		if stmt.Condition != nil { | |
| 			copied.Statement[i].Condition = make(map[string]map[string]interface{}) | |
| 			for k, v := range stmt.Condition { | |
| 				copied.Statement[i].Condition[k] = v | |
| 			} | |
| 		} | |
| 	} | |
| 
 | |
| 	return copied | |
| } | |
| 
 | |
| // FilerPolicyStore implements PolicyStore using SeaweedFS filer | |
| type FilerPolicyStore struct { | |
| 	filerGrpcAddress string | |
| 	grpcDialOption   grpc.DialOption | |
| 	basePath         string | |
| } | |
| 
 | |
| // NewFilerPolicyStore creates a new filer-based policy store | |
| func NewFilerPolicyStore(config map[string]interface{}) (*FilerPolicyStore, error) { | |
| 	store := &FilerPolicyStore{ | |
| 		basePath: "/etc/iam/policies", // Default path for policy storage - aligned with /etc/ convention | |
| 	} | |
| 
 | |
| 	// Parse configuration | |
| 	if config != nil { | |
| 		if filerAddr, ok := config["filerAddress"].(string); ok { | |
| 			store.filerGrpcAddress = filerAddr | |
| 		} | |
| 		if basePath, ok := config["basePath"].(string); ok { | |
| 			store.basePath = strings.TrimSuffix(basePath, "/") | |
| 		} | |
| 	} | |
| 
 | |
| 	// Validate configuration | |
| 	if store.filerGrpcAddress == "" { | |
| 		return nil, fmt.Errorf("filer address is required for FilerPolicyStore") | |
| 	} | |
| 
 | |
| 	glog.V(2).Infof("Initialized FilerPolicyStore with filer %s, basePath %s", | |
| 		store.filerGrpcAddress, store.basePath) | |
| 
 | |
| 	return store, nil | |
| } | |
| 
 | |
| // StorePolicy stores a policy document in filer | |
| func (s *FilerPolicyStore) StorePolicy(ctx context.Context, name string, policy *PolicyDocument) error { | |
| 	if name == "" { | |
| 		return fmt.Errorf("policy name cannot be empty") | |
| 	} | |
| 	if policy == nil { | |
| 		return fmt.Errorf("policy cannot be nil") | |
| 	} | |
| 
 | |
| 	// Serialize policy to JSON | |
| 	policyData, err := json.MarshalIndent(policy, "", "  ") | |
| 	if err != nil { | |
| 		return fmt.Errorf("failed to serialize policy: %v", err) | |
| 	} | |
| 
 | |
| 	policyPath := s.getPolicyPath(name) | |
| 
 | |
| 	// Store in filer | |
| 	return s.withFilerClient(func(client filer_pb.SeaweedFilerClient) error { | |
| 		request := &filer_pb.CreateEntryRequest{ | |
| 			Directory: s.basePath, | |
| 			Entry: &filer_pb.Entry{ | |
| 				Name:        s.getPolicyFileName(name), | |
| 				IsDirectory: false, | |
| 				Attributes: &filer_pb.FuseAttributes{ | |
| 					Mtime:    time.Now().Unix(), | |
| 					Crtime:   time.Now().Unix(), | |
| 					FileMode: uint32(0600), // Read/write for owner only | |
| 					Uid:      uint32(0), | |
| 					Gid:      uint32(0), | |
| 				}, | |
| 				Content: policyData, | |
| 			}, | |
| 		} | |
| 
 | |
| 		glog.V(3).Infof("Storing policy %s at %s", name, policyPath) | |
| 		_, err := client.CreateEntry(ctx, request) | |
| 		if err != nil { | |
| 			return fmt.Errorf("failed to store policy %s: %v", name, err) | |
| 		} | |
| 
 | |
| 		return nil | |
| 	}) | |
| } | |
| 
 | |
| // GetPolicy retrieves a policy document from filer | |
| func (s *FilerPolicyStore) GetPolicy(ctx context.Context, name string) (*PolicyDocument, error) { | |
| 	if name == "" { | |
| 		return nil, fmt.Errorf("policy name cannot be empty") | |
| 	} | |
| 
 | |
| 	var policyData []byte | |
| 	err := s.withFilerClient(func(client filer_pb.SeaweedFilerClient) error { | |
| 		request := &filer_pb.LookupDirectoryEntryRequest{ | |
| 			Directory: s.basePath, | |
| 			Name:      s.getPolicyFileName(name), | |
| 		} | |
| 
 | |
| 		glog.V(3).Infof("Looking up policy %s", name) | |
| 		response, err := client.LookupDirectoryEntry(ctx, request) | |
| 		if err != nil { | |
| 			return fmt.Errorf("policy not found: %v", err) | |
| 		} | |
| 
 | |
| 		if response.Entry == nil { | |
| 			return fmt.Errorf("policy not found") | |
| 		} | |
| 
 | |
| 		policyData = response.Entry.Content | |
| 		return nil | |
| 	}) | |
| 
 | |
| 	if err != nil { | |
| 		return nil, err | |
| 	} | |
| 
 | |
| 	// Deserialize policy from JSON | |
| 	var policy PolicyDocument | |
| 	if err := json.Unmarshal(policyData, &policy); err != nil { | |
| 		return nil, fmt.Errorf("failed to deserialize policy: %v", err) | |
| 	} | |
| 
 | |
| 	return &policy, nil | |
| } | |
| 
 | |
| // DeletePolicy deletes a policy document from filer | |
| func (s *FilerPolicyStore) DeletePolicy(ctx context.Context, name string) error { | |
| 	if name == "" { | |
| 		return fmt.Errorf("policy name cannot be empty") | |
| 	} | |
| 
 | |
| 	return s.withFilerClient(func(client filer_pb.SeaweedFilerClient) error { | |
| 		request := &filer_pb.DeleteEntryRequest{ | |
| 			Directory:            s.basePath, | |
| 			Name:                 s.getPolicyFileName(name), | |
| 			IsDeleteData:         true, | |
| 			IsRecursive:          false, | |
| 			IgnoreRecursiveError: false, | |
| 		} | |
| 
 | |
| 		glog.V(3).Infof("Deleting policy %s", name) | |
| 		resp, err := client.DeleteEntry(ctx, request) | |
| 		if err != nil { | |
| 			// Ignore "not found" errors - policy may already be deleted | |
| 			if strings.Contains(err.Error(), "not found") { | |
| 				return nil | |
| 			} | |
| 			return fmt.Errorf("failed to delete policy %s: %v", name, err) | |
| 		} | |
| 
 | |
| 		// Check response error | |
| 		if resp.Error != "" { | |
| 			// Ignore "not found" errors - policy may already be deleted | |
| 			if strings.Contains(resp.Error, "not found") { | |
| 				return nil | |
| 			} | |
| 			return fmt.Errorf("failed to delete policy %s: %s", name, resp.Error) | |
| 		} | |
| 
 | |
| 		return nil | |
| 	}) | |
| } | |
| 
 | |
| // ListPolicies lists all policy names in filer | |
| func (s *FilerPolicyStore) ListPolicies(ctx context.Context) ([]string, error) { | |
| 	var policyNames []string | |
| 
 | |
| 	err := s.withFilerClient(func(client filer_pb.SeaweedFilerClient) error { | |
| 		// List all entries in the policy directory | |
| 		request := &filer_pb.ListEntriesRequest{ | |
| 			Directory:          s.basePath, | |
| 			Prefix:             "policy_", | |
| 			StartFromFileName:  "", | |
| 			InclusiveStartFrom: false, | |
| 			Limit:              1000, // Process in batches of 1000 | |
| 		} | |
| 
 | |
| 		stream, err := client.ListEntries(ctx, request) | |
| 		if err != nil { | |
| 			return fmt.Errorf("failed to list policies: %v", err) | |
| 		} | |
| 
 | |
| 		for { | |
| 			resp, err := stream.Recv() | |
| 			if err != nil { | |
| 				break // End of stream or error | |
| 			} | |
| 
 | |
| 			if resp.Entry == nil || resp.Entry.IsDirectory { | |
| 				continue | |
| 			} | |
| 
 | |
| 			// Extract policy name from filename | |
| 			filename := resp.Entry.Name | |
| 			if strings.HasPrefix(filename, "policy_") && strings.HasSuffix(filename, ".json") { | |
| 				// Remove "policy_" prefix and ".json" suffix | |
| 				policyName := strings.TrimSuffix(strings.TrimPrefix(filename, "policy_"), ".json") | |
| 				policyNames = append(policyNames, policyName) | |
| 			} | |
| 		} | |
| 
 | |
| 		return nil | |
| 	}) | |
| 
 | |
| 	if err != nil { | |
| 		return nil, err | |
| 	} | |
| 
 | |
| 	return policyNames, nil | |
| } | |
| 
 | |
| // Helper methods | |
|  | |
| // SetFilerClient sets the filer client connection details | |
| func (s *FilerPolicyStore) SetFilerClient(filerAddress string, grpcDialOption grpc.DialOption) { | |
| 	s.filerGrpcAddress = filerAddress | |
| 	s.grpcDialOption = grpcDialOption | |
| } | |
| 
 | |
| // withFilerClient executes a function with a filer client | |
| func (s *FilerPolicyStore) withFilerClient(fn func(client filer_pb.SeaweedFilerClient) error) error { | |
| 	if s.filerGrpcAddress == "" { | |
| 		return fmt.Errorf("filer address not configured") | |
| 	} | |
| 
 | |
| 	// Use the pb.WithGrpcFilerClient helper similar to existing SeaweedFS code | |
| 	return pb.WithGrpcFilerClient(false, 0, pb.ServerAddress(s.filerGrpcAddress), s.grpcDialOption, fn) | |
| } | |
| 
 | |
| // getPolicyPath returns the full path for a policy | |
| func (s *FilerPolicyStore) getPolicyPath(policyName string) string { | |
| 	return s.basePath + "/" + s.getPolicyFileName(policyName) | |
| } | |
| 
 | |
| // getPolicyFileName returns the filename for a policy | |
| func (s *FilerPolicyStore) getPolicyFileName(policyName string) string { | |
| 	return "policy_" + policyName + ".json" | |
| }
 |