Skip to content

Commit 85c5be8

Browse files
authored
Merge pull request #42 from codeGROOVE-dev/reliable
disallow personal accounts by default, add refresh
2 parents 667db1a + b511f2b commit 85c5be8

File tree

6 files changed

+116
-21
lines changed

6 files changed

+116
-21
lines changed

cmd/server/main.go

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -148,7 +148,7 @@ func run(ctx context.Context, cancel context.CancelFunc, cfg *config.ServerConfi
148148
configManager := config.New(ctx)
149149

150150
// Initialize GitHub installation manager.
151-
githubManager, err := github.NewManager(ctx, cfg.GitHubAppID, cfg.GitHubPrivateKey)
151+
githubManager, err := github.NewManager(ctx, cfg.GitHubAppID, cfg.GitHubPrivateKey, cfg.AllowPersonalAccounts)
152152
if err != nil {
153153
slog.Error("failed to initialize GitHub installation manager", "error", err)
154154
cancel() // Ensure cleanup happens before exit
@@ -856,18 +856,23 @@ func loadConfig() (*config.ServerConfig, error) {
856856

857857
slog.Info("loading configuration values")
858858

859+
// Parse personal accounts flag (default: false for DoS protection)
860+
allowPersonalAccounts := os.Getenv("ALLOW_PERSONAL_ACCOUNTS") == "true"
861+
859862
cfg := &config.ServerConfig{
860-
DataDir: dataDir,
861-
SlackSigningSecret: getSecretValue("SLACK_SIGNING_SECRET"),
862-
GitHubAppID: os.Getenv("GITHUB_APP_ID"), // Not a secret, just config
863-
GitHubPrivateKey: githubPrivateKey,
864-
SprinklerURL: sprinklerURL,
863+
DataDir: dataDir,
864+
SlackSigningSecret: getSecretValue("SLACK_SIGNING_SECRET"),
865+
GitHubAppID: os.Getenv("GITHUB_APP_ID"), // Not a secret, just config
866+
GitHubPrivateKey: githubPrivateKey,
867+
SprinklerURL: sprinklerURL,
868+
AllowPersonalAccounts: allowPersonalAccounts,
865869
}
866870

867871
slog.Info("configuration loaded",
868872
"has_slack_signing_secret", cfg.SlackSigningSecret != "",
869873
"has_github_app_id", cfg.GitHubAppID != "",
870-
"has_github_private_key", cfg.GitHubPrivateKey != "")
874+
"has_github_private_key", cfg.GitHubPrivateKey != "",
875+
"allow_personal_accounts", cfg.AllowPersonalAccounts)
871876

872877
// Validate required fields
873878
if cfg.SlackSigningSecret == "" {

internal/config/config.go

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -28,11 +28,12 @@ const (
2828

2929
// ServerConfig holds the server configuration from environment variables.
3030
type ServerConfig struct {
31-
DataDir string
32-
SlackSigningSecret string
33-
GitHubAppID string
34-
GitHubPrivateKey string
35-
SprinklerURL string
31+
DataDir string
32+
SlackSigningSecret string
33+
GitHubAppID string
34+
GitHubPrivateKey string
35+
SprinklerURL string
36+
AllowPersonalAccounts bool // Allow processing GitHub personal accounts (default: false for DoS protection)
3637
}
3738

3839
// RepoConfig represents the slack.yaml configuration for a GitHub org.

internal/github/github.go

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -635,14 +635,15 @@ func (c *Client) InstallationToken(ctx context.Context) string {
635635

636636
// Manager manages multiple GitHub App installations.
637637
type Manager struct {
638-
privateKey *rsa.PrivateKey
639-
clients map[string]*Client // org -> client
640-
appID string
641-
mu sync.RWMutex
638+
privateKey *rsa.PrivateKey
639+
clients map[string]*Client // org -> client
640+
appID string
641+
allowPersonalAccounts bool // Allow processing personal accounts (default: false for DoS protection)
642+
mu sync.RWMutex
642643
}
643644

644645
// NewManager creates a new installation manager.
645-
func NewManager(ctx context.Context, appID, privateKeyPEM string) (*Manager, error) {
646+
func NewManager(ctx context.Context, appID, privateKeyPEM string, allowPersonalAccounts bool) (*Manager, error) {
646647
// Parse the private key.
647648
block, _ := pem.Decode([]byte(privateKeyPEM))
648649
if block == nil {
@@ -669,9 +670,10 @@ func NewManager(ctx context.Context, appID, privateKeyPEM string) (*Manager, err
669670
}
670671

671672
m := &Manager{
672-
clients: make(map[string]*Client),
673-
appID: appID,
674-
privateKey: key,
673+
clients: make(map[string]*Client),
674+
appID: appID,
675+
privateKey: key,
676+
allowPersonalAccounts: allowPersonalAccounts,
675677
}
676678

677679
// Discover installations at startup.
@@ -754,6 +756,14 @@ func (m *Manager) RefreshInstallations(ctx context.Context) error {
754756
continue
755757
}
756758

759+
// Skip personal accounts if not explicitly allowed (DoS protection)
760+
if !m.allowPersonalAccounts && inst.Account.GetType() == "User" {
761+
slog.Debug("skipping personal account",
762+
"account", inst.Account.GetLogin(),
763+
"type", "User")
764+
continue
765+
}
766+
757767
org := inst.Account.GetLogin()
758768

759769
// Create client for this installation.

internal/slack/home_handler.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,9 @@ func (h *HomeHandler) HandleAppHomeOpened(ctx context.Context, teamID, slackUser
9595
return h.publishPlaceholderHome(slackClient, slackUserID)
9696
}
9797

98+
// Add workspace orgs to dashboard for UI display
99+
dashboard.WorkspaceOrgs = workspaceOrgs
100+
98101
// Build Block Kit UI - use first org as primary
99102
blocks := home.BuildBlocks(dashboard, workspaceOrgs[0])
100103

internal/slack/slack.go

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -799,6 +799,7 @@ func (c *Client) EventsHandler(writer http.ResponseWriter, r *http.Request) {
799799
c.homeViewHandlerMu.RUnlock()
800800

801801
if handler != nil {
802+
//nolint:contextcheck // Use detached context for async event processing - prevents webhook events from being lost during shutdown
802803
go func(teamID, userID string) {
803804
homeCtx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
804805
defer cancel()
@@ -858,7 +859,8 @@ func (c *Client) InteractionsHandler(writer http.ResponseWriter, r *http.Request
858859
switch interaction.Type {
859860
case slack.InteractionTypeBlockActions:
860861
// Handle block actions (buttons, selects, etc.).
861-
slog.Debug("received block action", "interaction", interaction)
862+
//nolint:contextcheck // handleBlockAction spawns async goroutines with detached contexts - this is intentional
863+
c.handleBlockAction(&interaction)
862864
case slack.InteractionTypeViewSubmission:
863865
// Handle modal submissions.
864866
slog.Debug("received view submission", "interaction", interaction)
@@ -870,6 +872,51 @@ func (c *Client) InteractionsHandler(writer http.ResponseWriter, r *http.Request
870872
writer.WriteHeader(http.StatusOK)
871873
}
872874

875+
// handleBlockAction handles block action interactions (button clicks, etc.).
876+
func (c *Client) handleBlockAction(interaction *slack.InteractionCallback) {
877+
// Process each action in the callback
878+
for _, action := range interaction.ActionCallback.BlockActions {
879+
slog.Debug("processing block action",
880+
"action_id", action.ActionID,
881+
"user", interaction.User.ID,
882+
"team", interaction.Team.ID)
883+
884+
switch action.ActionID {
885+
case "refresh_dashboard":
886+
// Trigger home view refresh
887+
c.homeViewHandlerMu.RLock()
888+
handler := c.homeViewHandler
889+
c.homeViewHandlerMu.RUnlock()
890+
891+
if handler != nil {
892+
// Refresh asynchronously to avoid blocking the response
893+
//nolint:contextcheck // Use detached context for async button refresh - ensures operation completes even if parent context is cancelled
894+
go func(teamID, userID string) {
895+
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
896+
defer cancel()
897+
898+
slog.Info("refreshing dashboard via button click",
899+
"team_id", teamID,
900+
"user", userID)
901+
902+
if err := handler(ctx, teamID, userID); err != nil {
903+
slog.Error("failed to refresh dashboard",
904+
"team_id", teamID,
905+
"user", userID,
906+
"error", err)
907+
}
908+
}(interaction.Team.ID, interaction.User.ID)
909+
} else {
910+
slog.Warn("refresh requested but no home view handler registered",
911+
"user", interaction.User.ID)
912+
}
913+
914+
default:
915+
slog.Debug("unhandled action_id", "action_id", action.ActionID)
916+
}
917+
}
918+
}
919+
873920
// SlashCommandHandler handles Slack slash commands.
874921
func (c *Client) SlashCommandHandler(writer http.ResponseWriter, r *http.Request) {
875922
// Verify the request signature.

pkg/home/ui.go

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,35 @@ func BuildBlocks(dashboard *Dashboard, primaryOrg string) []slack.Block {
2020
),
2121
)
2222

23+
// Organization monitoring section - show which orgs this workspace tracks
24+
if len(dashboard.WorkspaceOrgs) > 0 {
25+
var orgLinks []string
26+
for _, org := range dashboard.WorkspaceOrgs {
27+
configURL := fmt.Sprintf("https://github.com/%s/.codeGROOVE/blob/main/slack.yaml", org)
28+
orgLinks = append(orgLinks, fmt.Sprintf("<%s|%s>", configURL, org))
29+
}
30+
orgsText := fmt.Sprintf("*Monitoring:* %s", strings.Join(orgLinks, " • "))
31+
32+
blocks = append(blocks,
33+
slack.NewContextBlock(
34+
"",
35+
slack.NewTextBlockObject("mrkdwn", orgsText, false, false),
36+
),
37+
)
38+
}
39+
40+
// Refresh button
41+
blocks = append(blocks,
42+
slack.NewActionBlock(
43+
"refresh_actions",
44+
slack.NewButtonBlockElement(
45+
"refresh_dashboard",
46+
"refresh",
47+
slack.NewTextBlockObject("plain_text", "🔄 Refresh", false, false),
48+
),
49+
),
50+
)
51+
2352
counts := dashboard.Counts()
2453

2554
// Incoming section - matches dashboard's card-based sections

0 commit comments

Comments
 (0)