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.
434 lines
12 KiB
434 lines
12 KiB
package dash
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/seaweedfs/seaweedfs/weed/glog"
|
|
"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 (
|
|
createdAtActionPrefix = "createdAt:"
|
|
expirationActionPrefix = "expiresAt:"
|
|
disabledAction = "__disabled__"
|
|
serviceAccountPrefix = "sa:"
|
|
accessKeyPrefix = "ABIA" // Service account access keys use ABIA prefix
|
|
|
|
// Status constants
|
|
StatusActive = "Active"
|
|
StatusInactive = "Inactive"
|
|
)
|
|
|
|
// Helper functions for managing creation timestamps in actions
|
|
func getCreationDate(actions []string) time.Time {
|
|
for _, action := range actions {
|
|
if strings.HasPrefix(action, createdAtActionPrefix) {
|
|
timestampStr := strings.TrimPrefix(action, createdAtActionPrefix)
|
|
if timestamp, err := strconv.ParseInt(timestampStr, 10, 64); err == nil {
|
|
return time.Unix(timestamp, 0)
|
|
}
|
|
}
|
|
}
|
|
return time.Time{} // Return zero time for legacy service accounts without stored creation date
|
|
}
|
|
|
|
func setCreationDate(actions []string, createDate time.Time) []string {
|
|
// Remove any existing createdAt action
|
|
filtered := make([]string, 0, len(actions)+1)
|
|
for _, action := range actions {
|
|
if !strings.HasPrefix(action, createdAtActionPrefix) {
|
|
filtered = append(filtered, action)
|
|
}
|
|
}
|
|
// Add new createdAt action
|
|
filtered = append(filtered, fmt.Sprintf("%s%d", createdAtActionPrefix, createDate.Unix()))
|
|
return filtered
|
|
}
|
|
|
|
// Helper functions for managing expiration timestamps in actions
|
|
func getExpiration(actions []string) time.Time {
|
|
for _, action := range actions {
|
|
if strings.HasPrefix(action, expirationActionPrefix) {
|
|
timestampStr := strings.TrimPrefix(action, expirationActionPrefix)
|
|
if timestamp, err := strconv.ParseInt(timestampStr, 10, 64); err == nil {
|
|
return time.Unix(timestamp, 0)
|
|
}
|
|
}
|
|
}
|
|
return time.Time{} // No expiration set
|
|
}
|
|
|
|
func setExpiration(actions []string, expiration time.Time) []string {
|
|
// Remove any existing expiration action
|
|
filtered := make([]string, 0, len(actions)+1)
|
|
for _, action := range actions {
|
|
if !strings.HasPrefix(action, expirationActionPrefix) {
|
|
filtered = append(filtered, action)
|
|
}
|
|
}
|
|
// Add new expiration action if not zero
|
|
if !expiration.IsZero() {
|
|
filtered = append(filtered, fmt.Sprintf("%s%d", expirationActionPrefix, expiration.Unix()))
|
|
}
|
|
return filtered
|
|
}
|
|
|
|
// GetServiceAccounts returns all service accounts, optionally filtered by parent user
|
|
// NOTE: Service accounts are stored as special identities with "sa:" prefix
|
|
func (s *AdminServer) GetServiceAccounts(ctx context.Context, parentUser string) ([]ServiceAccount, error) {
|
|
if s.credentialManager == nil {
|
|
return nil, fmt.Errorf("credential manager not available")
|
|
}
|
|
|
|
// Load the current configuration to find service account identities
|
|
config, err := s.credentialManager.LoadConfiguration(ctx)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to load configuration: %w", err)
|
|
}
|
|
|
|
var accounts []ServiceAccount
|
|
|
|
// Service accounts are stored as identities with "sa:" prefix in their name
|
|
// Format: "sa:<parent_user>:<uuid>"
|
|
for _, identity := range config.GetIdentities() {
|
|
if !strings.HasPrefix(identity.GetName(), serviceAccountPrefix) {
|
|
continue
|
|
}
|
|
|
|
parts := strings.SplitN(identity.GetName(), ":", 3)
|
|
if len(parts) < 3 {
|
|
continue
|
|
}
|
|
|
|
parent := parts[1]
|
|
saId := identity.GetName()
|
|
|
|
// Filter by parent user if specified
|
|
if parentUser != "" && parent != parentUser {
|
|
continue
|
|
}
|
|
|
|
// Extract description from account display name if available
|
|
description := ""
|
|
status := StatusActive
|
|
if identity.Account != nil {
|
|
description = identity.Account.GetDisplayName()
|
|
}
|
|
|
|
// Get access key from credentials
|
|
accessKey := ""
|
|
if len(identity.Credentials) > 0 {
|
|
accessKey = identity.Credentials[0].GetAccessKey()
|
|
// Service accounts use ABIA prefix
|
|
if !strings.HasPrefix(accessKey, accessKeyPrefix) {
|
|
continue // Not a service account
|
|
}
|
|
}
|
|
|
|
// Check if disabled (stored in actions)
|
|
for _, action := range identity.GetActions() {
|
|
if action == disabledAction {
|
|
status = StatusInactive
|
|
break
|
|
}
|
|
}
|
|
|
|
accounts = append(accounts, ServiceAccount{
|
|
ID: saId,
|
|
ParentUser: parent,
|
|
Description: description,
|
|
AccessKeyId: accessKey,
|
|
Status: status,
|
|
CreateDate: getCreationDate(identity.GetActions()),
|
|
Expiration: getExpiration(identity.GetActions()),
|
|
})
|
|
}
|
|
|
|
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")
|
|
}
|
|
|
|
// Get the identity
|
|
identity, err := s.credentialManager.GetUser(ctx, id)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("%w: %s", ErrServiceAccountNotFound, id)
|
|
}
|
|
|
|
if !strings.HasPrefix(identity.GetName(), serviceAccountPrefix) {
|
|
return nil, fmt.Errorf("%w: not a service account: %s", ErrServiceAccountNotFound, id)
|
|
}
|
|
|
|
parts := strings.SplitN(identity.GetName(), ":", 3)
|
|
if len(parts) < 3 {
|
|
return nil, fmt.Errorf("invalid service account ID format")
|
|
}
|
|
|
|
account := &ServiceAccount{
|
|
ID: id,
|
|
ParentUser: parts[1],
|
|
Status: StatusActive,
|
|
CreateDate: getCreationDate(identity.GetActions()),
|
|
Expiration: getExpiration(identity.GetActions()),
|
|
}
|
|
|
|
if identity.Account != nil {
|
|
account.Description = identity.Account.GetDisplayName()
|
|
}
|
|
|
|
if len(identity.Credentials) > 0 {
|
|
account.AccessKeyId = identity.Credentials[0].GetAccessKey()
|
|
}
|
|
|
|
// Check if disabled
|
|
for _, action := range identity.GetActions() {
|
|
if action == disabledAction {
|
|
account.Status = StatusInactive
|
|
break
|
|
}
|
|
}
|
|
|
|
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
|
|
_, err := s.credentialManager.GetUser(ctx, req.ParentUser)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("parent user not found: %s", req.ParentUser)
|
|
}
|
|
|
|
// Generate unique ID and credentials
|
|
uuid := generateAccountId()
|
|
saId := fmt.Sprintf("sa:%s:%s", req.ParentUser, uuid)
|
|
accessKey := accessKeyPrefix + generateAccessKey()[len(accessKeyPrefix):] // Use ABIA prefix for service accounts
|
|
secretKey := generateSecretKey()
|
|
|
|
// Create the service account as a special identity
|
|
now := time.Now()
|
|
|
|
// Parse expiration if provided
|
|
var expiration time.Time
|
|
if req.Expiration != "" {
|
|
var err error
|
|
expiration, err = time.Parse(time.RFC3339, req.Expiration)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("invalid expiration format: %w", err)
|
|
}
|
|
}
|
|
|
|
identity := &iam_pb.Identity{
|
|
Name: saId,
|
|
Account: &iam_pb.Account{
|
|
Id: uuid,
|
|
DisplayName: req.Description,
|
|
},
|
|
Credentials: []*iam_pb.Credential{
|
|
{
|
|
AccessKey: accessKey,
|
|
SecretKey: secretKey,
|
|
},
|
|
},
|
|
// Store creation date and expiration in actions
|
|
Actions: setExpiration(setCreationDate([]string{}, now), expiration),
|
|
}
|
|
|
|
// Create the service account
|
|
err = s.credentialManager.CreateUser(ctx, identity)
|
|
if 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)
|
|
|
|
return &ServiceAccount{
|
|
ID: saId,
|
|
ParentUser: req.ParentUser,
|
|
Description: req.Description,
|
|
AccessKeyId: accessKey,
|
|
SecretAccessKey: secretKey, // Only returned on creation
|
|
Status: StatusActive,
|
|
CreateDate: now,
|
|
Expiration: expiration,
|
|
}, 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")
|
|
}
|
|
|
|
// Get existing identity
|
|
identity, err := s.credentialManager.GetUser(ctx, id)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("%w: %s", ErrServiceAccountNotFound, id)
|
|
}
|
|
|
|
if !strings.HasPrefix(identity.GetName(), serviceAccountPrefix) {
|
|
return nil, fmt.Errorf("%w: not a service account: %s", ErrServiceAccountNotFound, id)
|
|
}
|
|
|
|
// Update description if provided
|
|
if req.Description != "" {
|
|
if identity.Account == nil {
|
|
identity.Account = &iam_pb.Account{}
|
|
}
|
|
identity.Account.DisplayName = req.Description
|
|
}
|
|
|
|
// Update status by adding/removing disabled action
|
|
if req.Status != "" {
|
|
// Remove existing disabled marker
|
|
newActions := make([]string, 0, len(identity.Actions))
|
|
for _, action := range identity.Actions {
|
|
if action != disabledAction {
|
|
newActions = append(newActions, action)
|
|
}
|
|
}
|
|
// Add disabled action if setting to Inactive
|
|
if req.Status == StatusInactive {
|
|
newActions = append(newActions, disabledAction)
|
|
}
|
|
identity.Actions = newActions
|
|
}
|
|
|
|
// Update expiration if provided
|
|
if req.Expiration != "" {
|
|
var expiration time.Time
|
|
var err error
|
|
expiration, err = time.Parse(time.RFC3339, req.Expiration)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("invalid expiration format: %w", err)
|
|
}
|
|
identity.Actions = setExpiration(identity.Actions, expiration)
|
|
}
|
|
|
|
// Update the identity
|
|
err = s.credentialManager.UpdateUser(ctx, id, identity)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to update service account: %w", err)
|
|
}
|
|
|
|
glog.V(1).Infof("Updated service account %s", id)
|
|
|
|
// Build response
|
|
parts := strings.SplitN(id, ":", 3)
|
|
if len(parts) < 3 {
|
|
return nil, fmt.Errorf("invalid service account ID format")
|
|
}
|
|
|
|
result := &ServiceAccount{
|
|
ID: id,
|
|
ParentUser: parts[1],
|
|
Description: identity.Account.GetDisplayName(),
|
|
Status: StatusActive,
|
|
CreateDate: getCreationDate(identity.Actions),
|
|
}
|
|
|
|
if len(identity.Credentials) > 0 {
|
|
result.AccessKeyId = identity.Credentials[0].GetAccessKey()
|
|
}
|
|
|
|
for _, action := range identity.Actions {
|
|
if action == disabledAction {
|
|
result.Status = StatusInactive
|
|
break
|
|
}
|
|
}
|
|
|
|
return result, 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 it's a service account
|
|
identity, err := s.credentialManager.GetUser(ctx, id)
|
|
if err != nil {
|
|
return fmt.Errorf("%w: %s", ErrServiceAccountNotFound, id)
|
|
}
|
|
|
|
if !strings.HasPrefix(identity.GetName(), serviceAccountPrefix) {
|
|
return fmt.Errorf("%w: not a service account: %s", ErrServiceAccountNotFound, id)
|
|
}
|
|
|
|
// Delete the identity
|
|
err = s.credentialManager.DeleteUser(ctx, id)
|
|
if 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")
|
|
}
|
|
|
|
// Find identity by access key
|
|
identity, err := s.credentialManager.GetUserByAccessKey(ctx, accessKey)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("service account not found for access key: %s", accessKey)
|
|
}
|
|
|
|
if !strings.HasPrefix(identity.GetName(), serviceAccountPrefix) {
|
|
return nil, fmt.Errorf("not a service account")
|
|
}
|
|
|
|
parts := strings.SplitN(identity.GetName(), ":", 3)
|
|
if len(parts) < 3 {
|
|
return nil, fmt.Errorf("invalid service account ID format")
|
|
}
|
|
|
|
account := &ServiceAccount{
|
|
ID: identity.GetName(),
|
|
ParentUser: parts[1],
|
|
AccessKeyId: accessKey,
|
|
Status: StatusActive,
|
|
CreateDate: getCreationDate(identity.GetActions()),
|
|
Expiration: getExpiration(identity.GetActions()),
|
|
}
|
|
|
|
if identity.Account != nil {
|
|
account.Description = identity.Account.GetDisplayName()
|
|
}
|
|
|
|
for _, action := range identity.GetActions() {
|
|
if action == disabledAction {
|
|
account.Status = StatusInactive
|
|
break
|
|
}
|
|
}
|
|
|
|
return account, nil
|
|
}
|