package access import ( "crypto/rand" "crypto/sha256" "crypto/subtle" "encoding/base64" "encoding/hex" "encoding/json" "errors" "fmt" "os" "path/filepath" "sort" "strings" "sync" "time" ) const ( RoleViewer = "project_viewer" RoleDeveloper = "project_developer" RoleAdmin = "project_admin" ) type ProjectMember struct { ProjectID string `json:"project_id"` UserID string `json:"user_id"` Role string `json:"role"` CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` } type ProjectToken struct { ID string `json:"id"` ProjectID string `json:"project_id"` Name string `json:"name"` Prefix string `json:"prefix"` Role string `json:"role"` SecretHash string `json:"-"` CreatedAt time.Time `json:"created_at"` ExpiresAt *time.Time `json:"expires_at,omitempty"` RevokedAt *time.Time `json:"revoked_at,omitempty"` } type persistedProjectToken struct { ID string `json:"id"` ProjectID string `json:"project_id"` Name string `json:"name"` Prefix string `json:"prefix"` Role string `json:"role"` SecretHash string `json:"secret_hash"` CreatedAt time.Time `json:"created_at"` ExpiresAt *time.Time `json:"expires_at,omitempty"` RevokedAt *time.Time `json:"revoked_at,omitempty"` } type database struct { Members []ProjectMember `json:"members"` Tokens []persistedProjectToken `json:"tokens"` } type Store struct { path string mu sync.RWMutex members map[string]map[string]ProjectMember tokens map[string]ProjectToken } func New(path string) (*Store, error) { if strings.TrimSpace(path) == "" { return nil, errors.New("项目权限数据文件不能为空") } store := &Store{ path: path, members: make(map[string]map[string]ProjectMember), tokens: make(map[string]ProjectToken), } if err := store.load(); err != nil { return nil, err } return store, nil } func ValidateRole(role string) error { switch strings.TrimSpace(role) { case RoleViewer, RoleDeveloper, RoleAdmin: return nil default: return errors.New("项目角色无效") } } func Allows(role, required string) bool { levels := map[string]int{RoleViewer: 1, RoleDeveloper: 2, RoleAdmin: 3} return levels[strings.TrimSpace(role)] >= levels[strings.TrimSpace(required)] && levels[strings.TrimSpace(required)] > 0 } func (s *Store) HasAccessControl(projectID string) bool { projectID = strings.TrimSpace(projectID) s.mu.RLock() defer s.mu.RUnlock() if len(s.members[projectID]) > 0 { return true } for _, token := range s.tokens { if token.ProjectID == projectID { return true } } return false } func (s *Store) ListMembers(projectID string) []ProjectMember { s.mu.RLock() defer s.mu.RUnlock() result := make([]ProjectMember, 0, len(s.members[projectID])) for _, member := range s.members[projectID] { result = append(result, member) } sort.Slice(result, func(i, j int) bool { return result[i].UserID < result[j].UserID }) return result } func (s *Store) GetMember(projectID, userID string) (ProjectMember, bool) { s.mu.RLock() defer s.mu.RUnlock() member, ok := s.members[strings.TrimSpace(projectID)][strings.TrimSpace(userID)] return member, ok } func (s *Store) AddMember(projectID, userID, role string) (ProjectMember, error) { projectID = strings.TrimSpace(projectID) userID = strings.TrimSpace(userID) role = strings.TrimSpace(role) if projectID == "" || userID == "" { return ProjectMember{}, errors.New("项目标识和用户标识不能为空") } if err := ValidateRole(role); err != nil { return ProjectMember{}, err } s.mu.Lock() defer s.mu.Unlock() if s.members[projectID] == nil { s.members[projectID] = make(map[string]ProjectMember) } if _, exists := s.members[projectID][userID]; exists { return ProjectMember{}, errors.New("项目成员已经存在") } now := time.Now().UTC() member := ProjectMember{ProjectID: projectID, UserID: userID, Role: role, CreatedAt: now, UpdatedAt: now} s.members[projectID][userID] = member if err := s.saveLocked(); err != nil { delete(s.members[projectID], userID) return ProjectMember{}, err } return member, nil } func (s *Store) UpdateMember(projectID, userID, role string) (ProjectMember, error) { projectID = strings.TrimSpace(projectID) userID = strings.TrimSpace(userID) role = strings.TrimSpace(role) if err := ValidateRole(role); err != nil { return ProjectMember{}, err } s.mu.Lock() defer s.mu.Unlock() member, exists := s.members[projectID][userID] if !exists { return ProjectMember{}, errors.New("项目成员不存在") } if member.Role == RoleAdmin && role != RoleAdmin && s.adminCountLocked(projectID) <= 1 { return ProjectMember{}, errors.New("项目至少需要保留一名管理员") } member.Role = role member.UpdatedAt = time.Now().UTC() s.members[projectID][userID] = member if err := s.saveLocked(); err != nil { return ProjectMember{}, err } return member, nil } func (s *Store) DeleteMember(projectID, userID string) error { projectID = strings.TrimSpace(projectID) userID = strings.TrimSpace(userID) s.mu.Lock() defer s.mu.Unlock() member, exists := s.members[projectID][userID] if !exists { return errors.New("项目成员不存在") } if member.Role == RoleAdmin && s.adminCountLocked(projectID) <= 1 { return errors.New("项目至少需要保留一名管理员") } delete(s.members[projectID], userID) return s.saveLocked() } func (s *Store) adminCountLocked(projectID string) int { count := 0 for _, member := range s.members[strings.TrimSpace(projectID)] { if member.Role == RoleAdmin { count++ } } return count } func (s *Store) CreateToken(projectID, name, role string, expiresAt *time.Time) (ProjectToken, string, error) { projectID = strings.TrimSpace(projectID) name = strings.TrimSpace(name) role = strings.TrimSpace(role) if projectID == "" || name == "" { return ProjectToken{}, "", errors.New("项目标识和令牌名称不能为空") } if len(name) > 80 { return ProjectToken{}, "", errors.New("令牌名称长度不能超过 80 个字符") } if err := ValidateRole(role); err != nil { return ProjectToken{}, "", err } if role == RoleAdmin { return ProjectToken{}, "", errors.New("项目令牌不能授予项目管理员权限") } secretBytes, err := randomBytes(32) if err != nil { return ProjectToken{}, "", fmt.Errorf("生成项目令牌失败: %w", err) } secret := "sct_" + base64.RawURLEncoding.EncodeToString(secretBytes) idBytes, err := randomBytes(16) if err != nil { return ProjectToken{}, "", fmt.Errorf("生成令牌标识失败: %w", err) } id := "tok_" + base64.RawURLEncoding.EncodeToString(idBytes) now := time.Now().UTC() token := ProjectToken{ ID: id, ProjectID: projectID, Name: name, Prefix: secret[:12], Role: role, SecretHash: hashSecret(secret), CreatedAt: now, ExpiresAt: expiresAt, } s.mu.Lock() s.tokens[id] = token if err := s.saveLocked(); err != nil { delete(s.tokens, id) s.mu.Unlock() return ProjectToken{}, "", err } safe := token safe.SecretHash = "" s.mu.Unlock() return safe, secret, nil } func (s *Store) ListTokens(projectID string) []ProjectToken { s.mu.RLock() defer s.mu.RUnlock() result := make([]ProjectToken, 0) for _, token := range s.tokens { if token.ProjectID != strings.TrimSpace(projectID) { continue } token.SecretHash = "" result = append(result, token) } sort.Slice(result, func(i, j int) bool { return result[i].CreatedAt.Before(result[j].CreatedAt) }) return result } func (s *Store) RevokeToken(projectID, tokenID string) (ProjectToken, error) { s.mu.Lock() defer s.mu.Unlock() token, exists := s.tokens[strings.TrimSpace(tokenID)] if !exists || token.ProjectID != strings.TrimSpace(projectID) { return ProjectToken{}, errors.New("项目令牌不存在") } if token.RevokedAt == nil { now := time.Now().UTC() token.RevokedAt = &now s.tokens[token.ID] = token if err := s.saveLocked(); err != nil { return ProjectToken{}, err } } token.SecretHash = "" return token, nil } func (s *Store) AuthenticateToken(projectID, secret string) (ProjectToken, bool) { projectID = strings.TrimSpace(projectID) secret = strings.TrimSpace(secret) if projectID == "" || secret == "" { return ProjectToken{}, false } hash := hashSecret(secret) now := time.Now().UTC() s.mu.RLock() defer s.mu.RUnlock() for _, token := range s.tokens { if token.ProjectID != projectID || token.RevokedAt != nil || (token.ExpiresAt != nil && now.After(*token.ExpiresAt)) { continue } if subtle.ConstantTimeCompare([]byte(token.SecretHash), []byte(hash)) == 1 { token.SecretHash = "" return token, true } } return ProjectToken{}, false } func (s *Store) load() error { data, err := os.ReadFile(s.path) if os.IsNotExist(err) { return nil } if err != nil { return fmt.Errorf("读取项目权限数据失败: %w", err) } var stored database if err := json.Unmarshal(data, &stored); err != nil { return fmt.Errorf("解析项目权限数据失败: %w", err) } for _, member := range stored.Members { if member.ProjectID == "" || member.UserID == "" || ValidateRole(member.Role) != nil { continue } if s.members[member.ProjectID] == nil { s.members[member.ProjectID] = make(map[string]ProjectMember) } s.members[member.ProjectID][member.UserID] = member } for _, token := range stored.Tokens { if token.ID != "" && token.ProjectID != "" && token.SecretHash != "" && ValidateRole(token.Role) == nil && token.Role != RoleAdmin { s.tokens[token.ID] = ProjectToken{ ID: token.ID, ProjectID: token.ProjectID, Name: token.Name, Prefix: token.Prefix, Role: token.Role, SecretHash: token.SecretHash, CreatedAt: token.CreatedAt, ExpiresAt: token.ExpiresAt, RevokedAt: token.RevokedAt, } } } return nil } func (s *Store) saveLocked() error { if err := os.MkdirAll(filepath.Dir(s.path), 0o755); err != nil { return fmt.Errorf("创建项目权限数据目录失败: %w", err) } stored := database{Members: make([]ProjectMember, 0), Tokens: make([]persistedProjectToken, 0, len(s.tokens))} for _, members := range s.members { for _, member := range members { stored.Members = append(stored.Members, member) } } for _, token := range s.tokens { stored.Tokens = append(stored.Tokens, persistedProjectToken{ ID: token.ID, ProjectID: token.ProjectID, Name: token.Name, Prefix: token.Prefix, Role: token.Role, SecretHash: token.SecretHash, CreatedAt: token.CreatedAt, ExpiresAt: token.ExpiresAt, RevokedAt: token.RevokedAt, }) } data, err := json.MarshalIndent(stored, "", " ") if err != nil { return fmt.Errorf("编码项目权限数据失败: %w", err) } temp, err := os.CreateTemp(filepath.Dir(s.path), ".project-access-*") if err != nil { return fmt.Errorf("创建项目权限临时文件失败: %w", err) } tempName := temp.Name() defer os.Remove(tempName) if _, err := temp.Write(data); err != nil { _ = temp.Close() return fmt.Errorf("写入项目权限数据失败: %w", err) } if err := temp.Close(); err != nil { return fmt.Errorf("关闭项目权限临时文件失败: %w", err) } if err := os.Rename(tempName, s.path); err != nil { return fmt.Errorf("保存项目权限数据失败: %w", err) } return nil } func randomBytes(size int) ([]byte, error) { value := make([]byte, size) _, err := rand.Read(value) return value, err } func hashSecret(secret string) string { hash := sha256.Sum256([]byte(secret)) return hex.EncodeToString(hash[:]) }