package dash import ( "context" "errors" "fmt" "strings" "time" "github.com/seaweedfs/seaweedfs/weed/glog" "github.com/seaweedfs/seaweedfs/weed/iam" "github.com/seaweedfs/seaweedfs/weed/pb/iam_pb" ) var ( // ErrServiceAccountNotFound is returned when a service account is not found ErrServiceAccountNotFound = errors.New("service account not found") ) const ( serviceAccountPrefix = "sa:" accessKeyPrefix = "ABIA" // Service account access keys use ABIA prefix // Status constants StatusActive = iam.AccessKeyStatusActive StatusInactive = iam.AccessKeyStatusInactive ) // GetServiceAccounts returns all service accounts, optionally filtered by parent user func (s *AdminServer) GetServiceAccounts(ctx context.Context, parentUser string) ([]ServiceAccount, error) { if s.credentialManager == nil { return nil, fmt.Errorf("credential manager not available") } pbAccounts, err := s.credentialManager.ListServiceAccounts(ctx) if err != nil { return nil, fmt.Errorf("failed to list service accounts: %w", err) } var accounts []ServiceAccount for _, sa := range pbAccounts { if sa == nil { continue } // Filter by parent user if specified if parentUser != "" && sa.ParentUser != parentUser { continue } status := StatusActive if sa.Disabled { status = StatusInactive } account := ServiceAccount{ ID: sa.Id, ParentUser: sa.ParentUser, Description: sa.Description, Status: status, CreateDate: time.Unix(sa.CreatedAt, 0), } if sa.Expiration > 0 { account.Expiration = time.Unix(sa.Expiration, 0) } if sa.Credential != nil { account.AccessKeyId = sa.Credential.AccessKey } accounts = append(accounts, account) } return accounts, nil } // GetServiceAccountDetails returns detailed information about a specific service account func (s *AdminServer) GetServiceAccountDetails(ctx context.Context, id string) (*ServiceAccount, error) { if s.credentialManager == nil { return nil, fmt.Errorf("credential manager not available") } sa, err := s.credentialManager.GetServiceAccount(ctx, id) if err != nil { return nil, fmt.Errorf("failed to get service account: %w", err) } if sa == nil { return nil, ErrServiceAccountNotFound } status := StatusActive if sa.Disabled { status = StatusInactive } account := &ServiceAccount{ ID: sa.Id, ParentUser: sa.ParentUser, Description: sa.Description, Status: status, CreateDate: time.Unix(sa.CreatedAt, 0), } if sa.Expiration > 0 { account.Expiration = time.Unix(sa.Expiration, 0) } if sa.Credential != nil { account.AccessKeyId = sa.Credential.AccessKey } return account, nil } // CreateServiceAccount creates a new service account for a parent user func (s *AdminServer) CreateServiceAccount(ctx context.Context, req CreateServiceAccountRequest) (*ServiceAccount, error) { if s.credentialManager == nil { return nil, fmt.Errorf("credential manager not available") } // Validate parent user exists if _, err := s.credentialManager.GetUser(ctx, req.ParentUser); err != nil { return nil, fmt.Errorf("parent user lookup failed for %s: %w", req.ParentUser, err) } // Generate unique ID and credentials uuid := generateAccountId() // Maintain consistent ID format: sa:: saId := fmt.Sprintf("sa:%s:%s", req.ParentUser, uuid) accessKey := accessKeyPrefix + generateAccessKey()[len(accessKeyPrefix):] secretKey := generateSecretKey() now := time.Now() sa := &iam_pb.ServiceAccount{ Id: saId, ParentUser: req.ParentUser, Description: req.Description, Credential: &iam_pb.Credential{ AccessKey: accessKey, SecretKey: secretKey, Status: StatusActive, }, CreatedAt: now.Unix(), Disabled: false, } if req.Expiration != "" { exp, err := time.Parse(time.RFC3339, req.Expiration) if err != nil { return nil, fmt.Errorf("invalid expiration format: %w", err) } sa.Expiration = exp.Unix() } if err := s.credentialManager.CreateServiceAccount(ctx, sa); err != nil { return nil, fmt.Errorf("failed to create service account: %w", err) } glog.V(1).Infof("Created service account %s for user %s", saId, req.ParentUser) resp := &ServiceAccount{ ID: saId, ParentUser: req.ParentUser, Description: req.Description, AccessKeyId: accessKey, SecretAccessKey: secretKey, // Only returned on creation Status: StatusActive, CreateDate: now, } if sa.Expiration > 0 { resp.Expiration = time.Unix(sa.Expiration, 0) } return resp, nil } // UpdateServiceAccount updates an existing service account func (s *AdminServer) UpdateServiceAccount(ctx context.Context, id string, req UpdateServiceAccountRequest) (*ServiceAccount, error) { if s.credentialManager == nil { return nil, fmt.Errorf("credential manager not available") } sa, err := s.credentialManager.GetServiceAccount(ctx, id) if err != nil { return nil, fmt.Errorf("failed to get service account: %w", err) } if sa == nil { return nil, ErrServiceAccountNotFound } if req.Description != "" { sa.Description = req.Description } if req.Status != "" { switch req.Status { case StatusInactive: sa.Disabled = true case StatusActive: sa.Disabled = false default: return nil, fmt.Errorf("invalid status value: %s (must be %s or %s)", req.Status, StatusActive, StatusInactive) } } if req.Expiration != "" { exp, err := time.Parse(time.RFC3339, req.Expiration) if err != nil { return nil, fmt.Errorf("invalid expiration format: %w", err) } sa.Expiration = exp.Unix() } if err := s.credentialManager.UpdateServiceAccount(ctx, id, sa); err != nil { return nil, fmt.Errorf("failed to update service account: %w", err) } glog.V(1).Infof("Updated service account %s", id) status := StatusActive if sa.Disabled { status = StatusInactive } accessKeyId := "" if sa.Credential != nil { accessKeyId = sa.Credential.AccessKey } resp := &ServiceAccount{ ID: sa.Id, ParentUser: sa.ParentUser, Description: sa.Description, Status: status, CreateDate: time.Unix(sa.CreatedAt, 0), AccessKeyId: accessKeyId, } if sa.Expiration > 0 { resp.Expiration = time.Unix(sa.Expiration, 0) } return resp, nil } // DeleteServiceAccount deletes a service account func (s *AdminServer) DeleteServiceAccount(ctx context.Context, id string) error { if s.credentialManager == nil { return fmt.Errorf("credential manager not available") } // Verify existence sa, err := s.credentialManager.GetServiceAccount(ctx, id) if err != nil { return fmt.Errorf("failed to check service account: %w", err) } if sa == nil { return ErrServiceAccountNotFound } if err := s.credentialManager.DeleteServiceAccount(ctx, id); err != nil { return fmt.Errorf("failed to delete service account: %w", err) } glog.V(1).Infof("Deleted service account %s", id) return nil } // GetServiceAccountByAccessKey finds a service account by its access key func (s *AdminServer) GetServiceAccountByAccessKey(ctx context.Context, accessKey string) (*ServiceAccount, error) { if !strings.HasPrefix(accessKey, accessKeyPrefix) { return nil, fmt.Errorf("not a service account access key") } if s.credentialManager == nil { return nil, fmt.Errorf("credential manager not available") } // Efficient lookup is now supported by the interface and optimized stores sa, err := s.credentialManager.GetStore().GetServiceAccountByAccessKey(ctx, accessKey) if err != nil { return nil, fmt.Errorf("failed to find service account: %w", err) } status := StatusActive if sa.Disabled { status = StatusInactive } accessKeyId := "" if sa.Credential != nil { accessKeyId = sa.Credential.AccessKey } resp := &ServiceAccount{ ID: sa.Id, ParentUser: sa.ParentUser, Description: sa.Description, AccessKeyId: accessKeyId, Status: status, CreateDate: time.Unix(sa.CreatedAt, 0), } if sa.Expiration > 0 { resp.Expiration = time.Unix(sa.Expiration, 0) } return resp, nil }