mirror of
https://github.com/memohai/Memoh.git
synced 2026-04-27 07:16:19 +09:00
feat(access): add guest chat ACL (#235)
This commit is contained in:
+9
-204
@@ -38,8 +38,7 @@ var (
|
||||
|
||||
// AccessPolicy controls bot access behavior.
|
||||
type AccessPolicy struct {
|
||||
AllowPublicMember bool
|
||||
AllowGuest bool
|
||||
AllowGuest bool
|
||||
}
|
||||
|
||||
// NewService creates a new bot service.
|
||||
@@ -87,12 +86,7 @@ func (s *Service) AuthorizeAccess(ctx context.Context, userID, botID string, isA
|
||||
return bot, nil
|
||||
}
|
||||
if bot.Type == BotTypePublic {
|
||||
if policy.AllowPublicMember {
|
||||
if _, err := s.GetMember(ctx, botID, userID); err == nil {
|
||||
return bot, nil
|
||||
}
|
||||
}
|
||||
if policy.AllowGuest && bot.AllowGuest {
|
||||
if policy.AllowGuest {
|
||||
return bot, nil
|
||||
}
|
||||
}
|
||||
@@ -209,57 +203,9 @@ func (s *Service) ListByOwner(ctx context.Context, ownerUserID string) ([]Bot, e
|
||||
return items, nil
|
||||
}
|
||||
|
||||
// ListByMember returns bots where the user is a member.
|
||||
func (s *Service) ListByMember(ctx context.Context, channelIdentityID string) ([]Bot, error) {
|
||||
if s.queries == nil {
|
||||
return nil, errors.New("bot queries not configured")
|
||||
}
|
||||
memberUUID, err := db.ParseUUID(channelIdentityID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
rows, err := s.queries.ListBotsByMember(ctx, memberUUID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items := make([]Bot, 0, len(rows))
|
||||
for _, row := range rows {
|
||||
item, err := toBot(asSQLCBot(row))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := s.attachCheckSummary(ctx, &item, asSQLCBot(row)); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, item)
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
// ListAccessible returns all bots the user can access (owned or member).
|
||||
// ListAccessible returns all bots owned by the user.
|
||||
func (s *Service) ListAccessible(ctx context.Context, channelIdentityID string) ([]Bot, error) {
|
||||
owned, err := s.ListByOwner(ctx, channelIdentityID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
members, err := s.ListByMember(ctx, channelIdentityID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
seen := map[string]Bot{}
|
||||
for _, item := range owned {
|
||||
seen[item.ID] = item
|
||||
}
|
||||
for _, item := range members {
|
||||
if _, ok := seen[item.ID]; !ok {
|
||||
seen[item.ID] = item
|
||||
}
|
||||
}
|
||||
items := make([]Bot, 0, len(seen))
|
||||
for _, item := range seen {
|
||||
items = append(items, item)
|
||||
}
|
||||
return items, nil
|
||||
return s.ListByOwner(ctx, channelIdentityID)
|
||||
}
|
||||
|
||||
// Update updates bot profile fields.
|
||||
@@ -485,118 +431,6 @@ func (s *Service) ensureUserExists(ctx context.Context, userID pgtype.UUID) erro
|
||||
return nil
|
||||
}
|
||||
|
||||
// UpsertMember creates or updates a bot membership.
|
||||
func (s *Service) UpsertMember(ctx context.Context, botID string, req UpsertMemberRequest) (BotMember, error) {
|
||||
if s.queries == nil {
|
||||
return BotMember{}, errors.New("bot queries not configured")
|
||||
}
|
||||
botUUID, err := db.ParseUUID(botID)
|
||||
if err != nil {
|
||||
return BotMember{}, err
|
||||
}
|
||||
memberUUID, err := db.ParseUUID(req.UserID)
|
||||
if err != nil {
|
||||
return BotMember{}, err
|
||||
}
|
||||
role, err := normalizeMemberRole(req.Role)
|
||||
if err != nil {
|
||||
return BotMember{}, err
|
||||
}
|
||||
row, err := s.queries.UpsertBotMember(ctx, sqlc.UpsertBotMemberParams{
|
||||
BotID: botUUID,
|
||||
UserID: memberUUID,
|
||||
Role: role,
|
||||
})
|
||||
if err != nil {
|
||||
return BotMember{}, err
|
||||
}
|
||||
return toBotMember(row), nil
|
||||
}
|
||||
|
||||
// ListMembers returns all members of a bot.
|
||||
func (s *Service) ListMembers(ctx context.Context, botID string) ([]BotMember, error) {
|
||||
if s.queries == nil {
|
||||
return nil, errors.New("bot queries not configured")
|
||||
}
|
||||
botUUID, err := db.ParseUUID(botID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
rows, err := s.queries.ListBotMembers(ctx, botUUID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items := make([]BotMember, 0, len(rows))
|
||||
for _, row := range rows {
|
||||
items = append(items, toBotMember(row))
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
// GetMember returns a specific bot member.
|
||||
func (s *Service) GetMember(ctx context.Context, botID, channelIdentityID string) (BotMember, error) {
|
||||
if s.queries == nil {
|
||||
return BotMember{}, errors.New("bot queries not configured")
|
||||
}
|
||||
botUUID, err := db.ParseUUID(botID)
|
||||
if err != nil {
|
||||
return BotMember{}, err
|
||||
}
|
||||
memberUUID, err := db.ParseUUID(channelIdentityID)
|
||||
if err != nil {
|
||||
return BotMember{}, err
|
||||
}
|
||||
row, err := s.queries.GetBotMember(ctx, sqlc.GetBotMemberParams{
|
||||
BotID: botUUID,
|
||||
UserID: memberUUID,
|
||||
})
|
||||
if err != nil {
|
||||
return BotMember{}, err
|
||||
}
|
||||
return toBotMember(row), nil
|
||||
}
|
||||
|
||||
// DeleteMember removes a member from a bot.
|
||||
func (s *Service) DeleteMember(ctx context.Context, botID, channelIdentityID string) error {
|
||||
if s.queries == nil {
|
||||
return errors.New("bot queries not configured")
|
||||
}
|
||||
botUUID, err := db.ParseUUID(botID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
memberUUID, err := db.ParseUUID(channelIdentityID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return s.queries.DeleteBotMember(ctx, sqlc.DeleteBotMemberParams{
|
||||
BotID: botUUID,
|
||||
UserID: memberUUID,
|
||||
})
|
||||
}
|
||||
|
||||
// UpsertMemberSimple creates or updates a bot membership with a direct channel identity ID and role.
|
||||
// This satisfies the router.BotMemberService interface.
|
||||
func (s *Service) UpsertMemberSimple(ctx context.Context, botID, channelIdentityID, role string) error {
|
||||
_, err := s.UpsertMember(ctx, botID, UpsertMemberRequest{
|
||||
UserID: channelIdentityID,
|
||||
Role: role,
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
// IsMember checks if a user is a member of a bot.
|
||||
func (s *Service) IsMember(ctx context.Context, botID, channelIdentityID string) (bool, error) {
|
||||
_, err := s.GetMember(ctx, botID, channelIdentityID)
|
||||
if err != nil {
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return false, nil
|
||||
}
|
||||
return false, err
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func normalizeBotType(raw string) (string, error) {
|
||||
normalized := strings.ToLower(strings.TrimSpace(raw))
|
||||
if normalized == "" {
|
||||
@@ -610,35 +444,20 @@ func normalizeBotType(raw string) (string, error) {
|
||||
}
|
||||
}
|
||||
|
||||
func normalizeMemberRole(raw string) (string, error) {
|
||||
role := strings.ToLower(strings.TrimSpace(raw))
|
||||
if role == "" {
|
||||
return MemberRoleMember, nil
|
||||
}
|
||||
switch role {
|
||||
case MemberRoleOwner, MemberRoleAdmin, MemberRoleMember:
|
||||
return role, nil
|
||||
default:
|
||||
return "", fmt.Errorf("invalid member role: %s", raw)
|
||||
}
|
||||
}
|
||||
|
||||
func asSQLCBot(v any) sqlc.Bot {
|
||||
switch r := v.(type) {
|
||||
case sqlc.Bot:
|
||||
return r
|
||||
case sqlc.CreateBotRow:
|
||||
return sqlc.Bot{ID: r.ID, OwnerUserID: r.OwnerUserID, Type: r.Type, DisplayName: r.DisplayName, AvatarUrl: r.AvatarUrl, IsActive: r.IsActive, Status: r.Status, MaxContextLoadTime: r.MaxContextLoadTime, MaxContextTokens: r.MaxContextTokens, MaxInboxItems: r.MaxInboxItems, Language: r.Language, AllowGuest: r.AllowGuest, ChatModelID: r.ChatModelID, SearchProviderID: r.SearchProviderID, MemoryProviderID: r.MemoryProviderID, Metadata: r.Metadata, CreatedAt: r.CreatedAt, UpdatedAt: r.UpdatedAt}
|
||||
return sqlc.Bot{ID: r.ID, OwnerUserID: r.OwnerUserID, Type: r.Type, DisplayName: r.DisplayName, AvatarUrl: r.AvatarUrl, IsActive: r.IsActive, Status: r.Status, MaxContextLoadTime: r.MaxContextLoadTime, MaxContextTokens: r.MaxContextTokens, MaxInboxItems: r.MaxInboxItems, Language: r.Language, ReasoningEnabled: r.ReasoningEnabled, ReasoningEffort: r.ReasoningEffort, ChatModelID: r.ChatModelID, SearchProviderID: r.SearchProviderID, MemoryProviderID: r.MemoryProviderID, HeartbeatEnabled: r.HeartbeatEnabled, HeartbeatInterval: r.HeartbeatInterval, HeartbeatPrompt: r.HeartbeatPrompt, Metadata: r.Metadata, CreatedAt: r.CreatedAt, UpdatedAt: r.UpdatedAt}
|
||||
case sqlc.GetBotByIDRow:
|
||||
return sqlc.Bot{ID: r.ID, OwnerUserID: r.OwnerUserID, Type: r.Type, DisplayName: r.DisplayName, AvatarUrl: r.AvatarUrl, IsActive: r.IsActive, Status: r.Status, MaxContextLoadTime: r.MaxContextLoadTime, MaxContextTokens: r.MaxContextTokens, MaxInboxItems: r.MaxInboxItems, Language: r.Language, AllowGuest: r.AllowGuest, ChatModelID: r.ChatModelID, SearchProviderID: r.SearchProviderID, MemoryProviderID: r.MemoryProviderID, Metadata: r.Metadata, CreatedAt: r.CreatedAt, UpdatedAt: r.UpdatedAt}
|
||||
return sqlc.Bot{ID: r.ID, OwnerUserID: r.OwnerUserID, Type: r.Type, DisplayName: r.DisplayName, AvatarUrl: r.AvatarUrl, IsActive: r.IsActive, Status: r.Status, MaxContextLoadTime: r.MaxContextLoadTime, MaxContextTokens: r.MaxContextTokens, MaxInboxItems: r.MaxInboxItems, Language: r.Language, ReasoningEnabled: r.ReasoningEnabled, ReasoningEffort: r.ReasoningEffort, ChatModelID: r.ChatModelID, SearchProviderID: r.SearchProviderID, MemoryProviderID: r.MemoryProviderID, HeartbeatEnabled: r.HeartbeatEnabled, HeartbeatInterval: r.HeartbeatInterval, HeartbeatPrompt: r.HeartbeatPrompt, Metadata: r.Metadata, CreatedAt: r.CreatedAt, UpdatedAt: r.UpdatedAt}
|
||||
case sqlc.ListBotsByOwnerRow:
|
||||
return sqlc.Bot{ID: r.ID, OwnerUserID: r.OwnerUserID, Type: r.Type, DisplayName: r.DisplayName, AvatarUrl: r.AvatarUrl, IsActive: r.IsActive, Status: r.Status, MaxContextLoadTime: r.MaxContextLoadTime, MaxContextTokens: r.MaxContextTokens, MaxInboxItems: r.MaxInboxItems, Language: r.Language, AllowGuest: r.AllowGuest, ChatModelID: r.ChatModelID, SearchProviderID: r.SearchProviderID, MemoryProviderID: r.MemoryProviderID, Metadata: r.Metadata, CreatedAt: r.CreatedAt, UpdatedAt: r.UpdatedAt}
|
||||
case sqlc.ListBotsByMemberRow:
|
||||
return sqlc.Bot{ID: r.ID, OwnerUserID: r.OwnerUserID, Type: r.Type, DisplayName: r.DisplayName, AvatarUrl: r.AvatarUrl, IsActive: r.IsActive, Status: r.Status, MaxContextLoadTime: r.MaxContextLoadTime, MaxContextTokens: r.MaxContextTokens, MaxInboxItems: r.MaxInboxItems, Language: r.Language, AllowGuest: r.AllowGuest, ChatModelID: r.ChatModelID, SearchProviderID: r.SearchProviderID, MemoryProviderID: r.MemoryProviderID, Metadata: r.Metadata, CreatedAt: r.CreatedAt, UpdatedAt: r.UpdatedAt}
|
||||
return sqlc.Bot{ID: r.ID, OwnerUserID: r.OwnerUserID, Type: r.Type, DisplayName: r.DisplayName, AvatarUrl: r.AvatarUrl, IsActive: r.IsActive, Status: r.Status, MaxContextLoadTime: r.MaxContextLoadTime, MaxContextTokens: r.MaxContextTokens, MaxInboxItems: r.MaxInboxItems, Language: r.Language, ReasoningEnabled: r.ReasoningEnabled, ReasoningEffort: r.ReasoningEffort, ChatModelID: r.ChatModelID, SearchProviderID: r.SearchProviderID, MemoryProviderID: r.MemoryProviderID, HeartbeatEnabled: r.HeartbeatEnabled, HeartbeatInterval: r.HeartbeatInterval, HeartbeatPrompt: r.HeartbeatPrompt, Metadata: r.Metadata, CreatedAt: r.CreatedAt, UpdatedAt: r.UpdatedAt}
|
||||
case sqlc.UpdateBotProfileRow:
|
||||
return sqlc.Bot{ID: r.ID, OwnerUserID: r.OwnerUserID, Type: r.Type, DisplayName: r.DisplayName, AvatarUrl: r.AvatarUrl, IsActive: r.IsActive, Status: r.Status, MaxContextLoadTime: r.MaxContextLoadTime, MaxContextTokens: r.MaxContextTokens, MaxInboxItems: r.MaxInboxItems, Language: r.Language, AllowGuest: r.AllowGuest, ChatModelID: r.ChatModelID, SearchProviderID: r.SearchProviderID, MemoryProviderID: r.MemoryProviderID, Metadata: r.Metadata, CreatedAt: r.CreatedAt, UpdatedAt: r.UpdatedAt}
|
||||
return sqlc.Bot{ID: r.ID, OwnerUserID: r.OwnerUserID, Type: r.Type, DisplayName: r.DisplayName, AvatarUrl: r.AvatarUrl, IsActive: r.IsActive, Status: r.Status, MaxContextLoadTime: r.MaxContextLoadTime, MaxContextTokens: r.MaxContextTokens, MaxInboxItems: r.MaxInboxItems, Language: r.Language, ReasoningEnabled: r.ReasoningEnabled, ReasoningEffort: r.ReasoningEffort, ChatModelID: r.ChatModelID, SearchProviderID: r.SearchProviderID, MemoryProviderID: r.MemoryProviderID, HeartbeatEnabled: r.HeartbeatEnabled, HeartbeatInterval: r.HeartbeatInterval, HeartbeatPrompt: r.HeartbeatPrompt, Metadata: r.Metadata, CreatedAt: r.CreatedAt, UpdatedAt: r.UpdatedAt}
|
||||
case sqlc.UpdateBotOwnerRow:
|
||||
return sqlc.Bot{ID: r.ID, OwnerUserID: r.OwnerUserID, Type: r.Type, DisplayName: r.DisplayName, AvatarUrl: r.AvatarUrl, IsActive: r.IsActive, Status: r.Status, MaxContextLoadTime: r.MaxContextLoadTime, MaxContextTokens: r.MaxContextTokens, MaxInboxItems: r.MaxInboxItems, Language: r.Language, AllowGuest: r.AllowGuest, ChatModelID: r.ChatModelID, SearchProviderID: r.SearchProviderID, MemoryProviderID: r.MemoryProviderID, Metadata: r.Metadata, CreatedAt: r.CreatedAt, UpdatedAt: r.UpdatedAt}
|
||||
return sqlc.Bot{ID: r.ID, OwnerUserID: r.OwnerUserID, Type: r.Type, DisplayName: r.DisplayName, AvatarUrl: r.AvatarUrl, IsActive: r.IsActive, Status: r.Status, MaxContextLoadTime: r.MaxContextLoadTime, MaxContextTokens: r.MaxContextTokens, MaxInboxItems: r.MaxInboxItems, Language: r.Language, ReasoningEnabled: r.ReasoningEnabled, ReasoningEffort: r.ReasoningEffort, ChatModelID: r.ChatModelID, SearchProviderID: r.SearchProviderID, MemoryProviderID: r.MemoryProviderID, HeartbeatEnabled: r.HeartbeatEnabled, HeartbeatInterval: r.HeartbeatInterval, HeartbeatPrompt: r.HeartbeatPrompt, Metadata: r.Metadata, CreatedAt: r.CreatedAt, UpdatedAt: r.UpdatedAt}
|
||||
default:
|
||||
return sqlc.Bot{}
|
||||
}
|
||||
@@ -672,7 +491,6 @@ func toBot(row sqlc.Bot) (Bot, error) {
|
||||
DisplayName: displayName,
|
||||
AvatarURL: avatarURL,
|
||||
IsActive: row.IsActive,
|
||||
AllowGuest: row.AllowGuest,
|
||||
Status: strings.TrimSpace(row.Status),
|
||||
CheckState: BotCheckStateUnknown,
|
||||
CheckIssueCount: 0,
|
||||
@@ -682,19 +500,6 @@ func toBot(row sqlc.Bot) (Bot, error) {
|
||||
}, nil
|
||||
}
|
||||
|
||||
func toBotMember(row sqlc.BotMember) BotMember {
|
||||
createdAt := time.Time{}
|
||||
if row.CreatedAt.Valid {
|
||||
createdAt = row.CreatedAt.Time
|
||||
}
|
||||
return BotMember{
|
||||
BotID: row.BotID.String(),
|
||||
UserID: row.UserID.String(),
|
||||
Role: row.Role,
|
||||
CreatedAt: createdAt,
|
||||
}
|
||||
}
|
||||
|
||||
func decodeMetadata(payload []byte) (map[string]any, error) {
|
||||
if len(payload) == 0 {
|
||||
return map[string]any{}, nil
|
||||
|
||||
@@ -42,13 +42,13 @@ func (d *fakeDBTX) QueryRow(ctx context.Context, sql string, args ...any) pgx.Ro
|
||||
|
||||
// makeBotRow creates a fakeRow that populates a sqlc.Bot via Scan.
|
||||
// Column order: id, owner_user_id, type, display_name, avatar_url, is_active, status,
|
||||
// max_context_load_time, max_context_tokens, max_inbox_items, language, allow_guest,
|
||||
// max_context_load_time, max_context_tokens, max_inbox_items, language,
|
||||
// reasoning_enabled, reasoning_effort, chat_model_id, search_provider_id, memory_provider_id,
|
||||
// heartbeat_enabled, heartbeat_interval, heartbeat_prompt, metadata, created_at, updated_at.
|
||||
func makeBotRow(botID, ownerUserID pgtype.UUID, botType string, allowGuest bool) *fakeRow {
|
||||
return &fakeRow{
|
||||
scanFunc: func(dest ...any) error {
|
||||
if len(dest) < 23 {
|
||||
if len(dest) < 22 {
|
||||
return pgx.ErrNoRows
|
||||
}
|
||||
*dest[0].(*pgtype.UUID) = botID
|
||||
@@ -62,42 +62,23 @@ func makeBotRow(botID, ownerUserID pgtype.UUID, botType string, allowGuest bool)
|
||||
*dest[8].(*int32) = 4096 // MaxContextTokens
|
||||
*dest[9].(*int32) = 10 // MaxInboxItems
|
||||
*dest[10].(*string) = "en"
|
||||
*dest[11].(*bool) = allowGuest
|
||||
*dest[12].(*bool) = false // ReasoningEnabled
|
||||
*dest[13].(*string) = "medium" // ReasoningEffort
|
||||
*dest[14].(*pgtype.UUID) = pgtype.UUID{} // ChatModelID
|
||||
*dest[15].(*pgtype.UUID) = pgtype.UUID{} // SearchProviderID
|
||||
*dest[16].(*pgtype.UUID) = pgtype.UUID{} // MemoryProviderID
|
||||
*dest[17].(*bool) = false // HeartbeatEnabled
|
||||
*dest[18].(*int32) = 30 // HeartbeatInterval
|
||||
*dest[19].(*string) = "" // HeartbeatPrompt
|
||||
*dest[20].(*[]byte) = []byte(`{}`)
|
||||
_ = allowGuest
|
||||
*dest[11].(*bool) = false // ReasoningEnabled
|
||||
*dest[12].(*string) = "medium" // ReasoningEffort
|
||||
*dest[13].(*pgtype.UUID) = pgtype.UUID{} // ChatModelID
|
||||
*dest[14].(*pgtype.UUID) = pgtype.UUID{} // SearchProviderID
|
||||
*dest[15].(*pgtype.UUID) = pgtype.UUID{} // MemoryProviderID
|
||||
*dest[16].(*bool) = false // HeartbeatEnabled
|
||||
*dest[17].(*int32) = 30 // HeartbeatInterval
|
||||
*dest[18].(*string) = "" // HeartbeatPrompt
|
||||
*dest[19].(*[]byte) = []byte(`{}`)
|
||||
*dest[20].(*pgtype.Timestamptz) = pgtype.Timestamptz{}
|
||||
*dest[21].(*pgtype.Timestamptz) = pgtype.Timestamptz{}
|
||||
*dest[22].(*pgtype.Timestamptz) = pgtype.Timestamptz{}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func makeMemberRow(botID, userID pgtype.UUID) *fakeRow {
|
||||
return &fakeRow{
|
||||
scanFunc: func(dest ...any) error {
|
||||
if len(dest) < 4 {
|
||||
return pgx.ErrNoRows
|
||||
}
|
||||
*dest[0].(*pgtype.UUID) = botID
|
||||
*dest[1].(*pgtype.UUID) = userID
|
||||
*dest[2].(*string) = MemberRoleMember
|
||||
*dest[3].(*pgtype.Timestamptz) = pgtype.Timestamptz{}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func makeNoRow() *fakeRow {
|
||||
return &fakeRow{scanFunc: func(_ ...any) error { return pgx.ErrNoRows }}
|
||||
}
|
||||
|
||||
func mustParseUUID(s string) pgtype.UUID {
|
||||
var u pgtype.UUID
|
||||
_ = u.Scan(s)
|
||||
@@ -108,12 +89,9 @@ func TestAuthorizeAccess(t *testing.T) {
|
||||
ownerUUID := mustParseUUID("00000000-0000-0000-0000-000000000001")
|
||||
botUUID := mustParseUUID("00000000-0000-0000-0000-000000000002")
|
||||
strangerUUID := mustParseUUID("00000000-0000-0000-0000-000000000003")
|
||||
memberUUID := mustParseUUID("00000000-0000-0000-0000-000000000004")
|
||||
|
||||
ownerID := ownerUUID.String()
|
||||
botID := botUUID.String()
|
||||
strangerID := strangerUUID.String()
|
||||
memberID := memberUUID.String()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
@@ -122,7 +100,6 @@ func TestAuthorizeAccess(t *testing.T) {
|
||||
policy AccessPolicy
|
||||
botType string
|
||||
allowGst bool
|
||||
isMember bool
|
||||
wantErr bool
|
||||
wantErrIs error
|
||||
}{
|
||||
@@ -146,50 +123,21 @@ func TestAuthorizeAccess(t *testing.T) {
|
||||
userID: strangerID,
|
||||
policy: AccessPolicy{AllowGuest: false},
|
||||
botType: BotTypePublic,
|
||||
allowGst: true,
|
||||
wantErr: true,
|
||||
wantErrIs: ErrBotAccessDenied,
|
||||
},
|
||||
{
|
||||
name: "stranger denied when bot guest disabled",
|
||||
userID: strangerID,
|
||||
policy: AccessPolicy{AllowGuest: true},
|
||||
botType: BotTypePublic,
|
||||
allowGst: false,
|
||||
wantErr: true,
|
||||
wantErrIs: ErrBotAccessDenied,
|
||||
},
|
||||
{
|
||||
name: "stranger allowed when policy and bot both allow guest",
|
||||
userID: strangerID,
|
||||
policy: AccessPolicy{AllowGuest: true},
|
||||
botType: BotTypePublic,
|
||||
allowGst: true,
|
||||
wantErr: false,
|
||||
name: "stranger allowed when policy allows guest",
|
||||
userID: strangerID,
|
||||
policy: AccessPolicy{AllowGuest: true},
|
||||
botType: BotTypePublic,
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "guest not allowed on personal bot",
|
||||
userID: strangerID,
|
||||
policy: AccessPolicy{AllowGuest: true},
|
||||
botType: BotTypePersonal,
|
||||
allowGst: true,
|
||||
wantErr: true,
|
||||
wantErrIs: ErrBotAccessDenied,
|
||||
},
|
||||
{
|
||||
name: "member allowed with AllowPublicMember policy",
|
||||
userID: memberID,
|
||||
policy: AccessPolicy{AllowPublicMember: true},
|
||||
botType: BotTypePublic,
|
||||
isMember: true,
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "non-member denied with AllowPublicMember policy",
|
||||
userID: strangerID,
|
||||
policy: AccessPolicy{AllowPublicMember: true},
|
||||
botType: BotTypePublic,
|
||||
isMember: false,
|
||||
wantErr: true,
|
||||
wantErrIs: ErrBotAccessDenied,
|
||||
},
|
||||
@@ -199,14 +147,8 @@ func TestAuthorizeAccess(t *testing.T) {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
db := &fakeDBTX{
|
||||
queryRowFunc: func(_ context.Context, _ string, args ...any) pgx.Row {
|
||||
// Route to bot or member row based on query.
|
||||
if len(args) == 1 {
|
||||
return makeBotRow(botUUID, ownerUUID, tt.botType, tt.allowGst)
|
||||
}
|
||||
if tt.isMember {
|
||||
return makeMemberRow(botUUID, memberUUID)
|
||||
}
|
||||
return makeNoRow()
|
||||
_ = args
|
||||
return makeBotRow(botUUID, ownerUUID, tt.botType, tt.allowGst)
|
||||
},
|
||||
}
|
||||
svc := NewService(nil, sqlc.New(db))
|
||||
|
||||
@@ -13,7 +13,6 @@ type Bot struct {
|
||||
DisplayName string `json:"display_name"`
|
||||
AvatarURL string `json:"avatar_url,omitempty"`
|
||||
IsActive bool `json:"is_active"`
|
||||
AllowGuest bool `json:"allow_guest"`
|
||||
Status string `json:"status"`
|
||||
CheckState string `json:"check_state"`
|
||||
CheckIssueCount int32 `json:"check_issue_count"`
|
||||
@@ -22,14 +21,6 @@ type Bot struct {
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
// BotMember represents a bot membership record.
|
||||
type BotMember struct {
|
||||
BotID string `json:"bot_id"`
|
||||
UserID string `json:"user_id"`
|
||||
Role string `json:"role"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
}
|
||||
|
||||
// BotCheck represents one resource check row for a bot.
|
||||
type BotCheck struct {
|
||||
ID string `json:"id"`
|
||||
@@ -64,22 +55,11 @@ type TransferBotRequest struct {
|
||||
OwnerUserID string `json:"owner_user_id"`
|
||||
}
|
||||
|
||||
// UpsertMemberRequest is the input for upserting a bot member.
|
||||
type UpsertMemberRequest struct {
|
||||
UserID string `json:"user_id"`
|
||||
Role string `json:"role,omitempty"`
|
||||
}
|
||||
|
||||
// ListBotsResponse wraps a list of bots.
|
||||
type ListBotsResponse struct {
|
||||
Items []Bot `json:"items"`
|
||||
}
|
||||
|
||||
// ListMembersResponse wraps a list of bot members.
|
||||
type ListMembersResponse struct {
|
||||
Items []BotMember `json:"items"`
|
||||
}
|
||||
|
||||
// ListChecksResponse wraps a list of bot checks.
|
||||
type ListChecksResponse struct {
|
||||
Items []BotCheck `json:"items"`
|
||||
@@ -130,9 +110,3 @@ const (
|
||||
BotCheckTypeMCPConnection = "mcp.connection"
|
||||
BotCheckTypeChannelConn = "channel.connection"
|
||||
)
|
||||
|
||||
const (
|
||||
MemberRoleOwner = "owner"
|
||||
MemberRoleAdmin = "admin"
|
||||
MemberRoleMember = "member"
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user