Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
bb59b3c
pipeline: probe slack for the channel manager role id
0xDracula Aug 30, 2026
8207d9d
db: add the api schema and channel manager assignments
0xDracula Aug 30, 2026
7ba921c
web(fd): split the audit log by source
0xDracula Aug 30, 2026
760e802
db: add the public_api flag
0xDracula Aug 30, 2026
a1fe117
web: let a member without a role sign in
0xDracula Aug 30, 2026
f50965b
web: add the permissions list on /you/api
0xDracula Aug 30, 2026
0f97ff6
web(fd): show consent changes in the audit log
0xDracula Aug 30, 2026
4f1af4a
web: add the tokens tab on /you/api
0xDracula Aug 30, 2026
6fdd053
web: authenticate api v1 with bearer tokens
0xDracula Aug 30, 2026
b7c03bb
web: answer channel manager checks
0xDracula Aug 30, 2026
34e8b8e
web: rate limit api tokens
0xDracula Aug 30, 2026
7c8b8c7
web(fd): add the api settings tab
0xDracula Aug 30, 2026
4589530
web(fd): roll up api requests into the audit log
0xDracula Aug 30, 2026
28a4134
web(docs): channel managers' api
0xDracula Aug 30, 2026
6a26c78
web(css): generate token modal
0xDracula Aug 30, 2026
0923c70
web(fd): tidy the api settings copy
0xDracula Aug 31, 2026
797be17
fix(nemo): keep case chat that has no member to reach
0xDracula Oct 2, 2026
8c77681
fix(nemo): stop selecting the dropped participant detail
0xDracula Oct 2, 2026
f7859db
refactor(db): renumber the api migrations onto 0153
0xDracula Oct 2, 2026
4d044ab
refactor(web): name the api consent scopes api_scopes
0xDracula Oct 2, 2026
2bad22f
chore: merge
0xDracula Oct 2, 2026
377f511
refactor(web): port the api branch onto Account and Authz
0xDracula Oct 2, 2026
b5b7150
feat(web): gate the api behind per-app approval and consent
0xDracula Oct 2, 2026
29bd8d6
fix: meow
0xDracula Oct 2, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions db/api_scopes.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
scopes:
channel_manager:
label: Channel manager lookups
covers: Apps can ask whether you manage a given channel.
6 changes: 6 additions & 0 deletions db/capabilities.yml
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ areas:
slack: Your own account
access: Access
app: The dashboard
api: Public API

record_scopes: [author, channel]

Expand Down Expand Up @@ -253,3 +254,8 @@ capabilities:
area: app
locked: true
events: [automod_word/added, automod_word/removed, app_setting/tuned]

api.review:
label: Review requests for public API access
area: api
events: [access/requested, access/approved, access/declined]
6 changes: 6 additions & 0 deletions db/flags.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,9 @@ flags:
label: Fire Engine
covers: Cases, Members and the search palette
default: true
public_api:
label: Public API
covers: Channel manager lookups, tokens and opt-in
audience: Everyone in Slack
default: false
listed: false
20 changes: 20 additions & 0 deletions db/init.sql
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ CREATE SCHEMA IF NOT EXISTS raw;
CREATE SCHEMA IF NOT EXISTS analytics;
CREATE SCHEMA IF NOT EXISTS app;
CREATE SCHEMA IF NOT EXISTS fd;
CREATE SCHEMA IF NOT EXISTS api;
CREATE SCHEMA IF NOT EXISTS ingest;
CREATE SCHEMA IF NOT EXISTS archive;

Expand Down Expand Up @@ -104,6 +105,25 @@ BEGIN
EXECUTE 'GRANT USAGE ON ALL SEQUENCES IN SCHEMA fd TO rails_app';
EXECUTE 'ALTER DEFAULT PRIVILEGES IN SCHEMA fd GRANT USAGE ON SEQUENCES TO rails_app';

EXECUTE 'GRANT USAGE ON SCHEMA api TO rails_app';
EXECUTE 'GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA api TO rails_app';
EXECUTE 'ALTER DEFAULT PRIVILEGES IN SCHEMA api '
'GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO rails_app';
EXECUTE 'GRANT USAGE ON ALL SEQUENCES IN SCHEMA api TO rails_app';
EXECUTE 'ALTER DEFAULT PRIVILEGES IN SCHEMA api GRANT USAGE ON SEQUENCES TO rails_app';

EXECUTE 'GRANT USAGE ON SCHEMA api TO pipeline_writer';
EXECUTE 'GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA api '
'TO pipeline_writer';
EXECUTE 'ALTER DEFAULT PRIVILEGES IN SCHEMA api '
'GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO pipeline_writer';
EXECUTE 'GRANT USAGE ON ALL SEQUENCES IN SCHEMA api TO pipeline_writer';
EXECUTE 'ALTER DEFAULT PRIVILEGES IN SCHEMA api '
'GRANT USAGE ON SEQUENCES TO pipeline_writer';

EXECUTE 'REVOKE ALL ON ALL TABLES IN SCHEMA api FROM dbt_owner';
EXECUTE 'REVOKE ALL ON SCHEMA api FROM dbt_owner';

IF EXISTS (SELECT 1 FROM pg_tables WHERE schemaname = 'app' AND tablename = 'sync_request') THEN
EXECUTE 'GRANT SELECT, UPDATE ON app.sync_request TO pipeline_writer';
END IF;
Expand Down
82 changes: 82 additions & 0 deletions db/migrations/0153_api_and_channel_managers.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
CREATE SCHEMA IF NOT EXISTS api;

CREATE TABLE api.channel_manager (
channel_id text NOT NULL,
user_id text NOT NULL,
assigned_at timestamptz,
PRIMARY KEY (channel_id, user_id)
);

CREATE INDEX channel_manager_user_idx ON api.channel_manager (user_id);

CREATE TABLE api.channel_sweep (
channel_id text PRIMARY KEY,
synced_at timestamptz NOT NULL DEFAULT now(),
managers integer NOT NULL DEFAULT 0
);

CREATE TABLE api.consent (
user_id text NOT NULL,
capability text NOT NULL,
state text NOT NULL,
changed_at timestamptz NOT NULL DEFAULT now(),
changed_via text NOT NULL,
first_granted_at timestamptz,
PRIMARY KEY (user_id, capability),
CONSTRAINT consent_state CHECK (state IN ('granted', 'withheld'))
);

CREATE INDEX consent_granted_idx ON api.consent (capability)
WHERE state = 'granted';

CREATE TABLE api.consent_log (
id bigserial PRIMARY KEY,
user_id text NOT NULL,
capability text NOT NULL,
state text NOT NULL,
via text NOT NULL,
at timestamptz NOT NULL DEFAULT now()
);

CREATE INDEX consent_log_at_idx ON api.consent_log (at DESC);

CREATE TABLE api.token (
id bigserial PRIMARY KEY,
owner_user_id text NOT NULL,
name text NOT NULL,
prefix text NOT NULL,
digest text NOT NULL UNIQUE,
rate_limit integer,
created_at timestamptz NOT NULL DEFAULT now(),
last_used_at timestamptz,
revoked_at timestamptz,
revoked_by text,
CONSTRAINT token_rate_limit_sane CHECK (rate_limit IS NULL OR rate_limit > 0)
);

CREATE INDEX token_owner_idx ON api.token (owner_user_id)
WHERE revoked_at IS NULL;

CREATE TABLE api.setting (
key text PRIMARY KEY,
value integer NOT NULL,
changed_by text,
changed_at timestamptz NOT NULL DEFAULT now()
);

INSERT INTO api.setting (key, value) VALUES
('rate_per_minute', 100),
('batch_max', 100),
('tokens_per_owner', 3);

CREATE TABLE api.request_log (
id bigserial PRIMARY KEY,
token_id bigint NOT NULL REFERENCES api.token (id),
channel_id text,
subject_user_id text,
outcome text NOT NULL,
at timestamptz NOT NULL DEFAULT now()
);

CREATE INDEX request_log_subject_idx ON api.request_log (subject_user_id, at DESC);
CREATE INDEX request_log_token_idx ON api.request_log (token_id, at DESC);
10 changes: 10 additions & 0 deletions db/migrations/0154_api_event_log.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
CREATE TABLE api.event_log (
id bigserial PRIMARY KEY,
actor_user_id text,
verb text NOT NULL,
subject text,
detail text,
at timestamptz NOT NULL DEFAULT now()
);

CREATE INDEX event_log_at_idx ON api.event_log (at DESC);
4 changes: 4 additions & 0 deletions db/migrations/0155_api_rate_default.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
UPDATE api.setting
SET value = 20
WHERE key = 'rate_per_minute'
AND value = 100;
1 change: 1 addition & 0 deletions db/migrations/0156_api_drop_batch_max.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
DELETE FROM api.setting WHERE key = 'batch_max';
4 changes: 4 additions & 0 deletions db/migrations/0157_api_token_expiry.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
ALTER TABLE api.token ADD COLUMN expires_at timestamptz;

CREATE INDEX token_expiry_idx ON api.token (expires_at)
WHERE revoked_at IS NULL AND expires_at IS NOT NULL;
37 changes: 37 additions & 0 deletions db/migrations/0158_api_access_requests.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
CREATE TABLE api.access_request (
id bigserial PRIMARY KEY,
user_id text NOT NULL,
scope text NOT NULL,
reason text NOT NULL,
state text NOT NULL DEFAULT 'pending',
created_at timestamptz NOT NULL DEFAULT now(),
decided_by text,
decided_at timestamptz,
note text,
CONSTRAINT access_request_state_known
CHECK (state IN ('pending', 'approved', 'declined', 'withdrawn')),
CONSTRAINT access_request_decided_together
CHECK ((state = 'pending') = (decided_at IS NULL))
);

CREATE UNIQUE INDEX access_request_one_open_idx
ON api.access_request (user_id, scope) WHERE state = 'pending';

CREATE INDEX access_request_queue_idx
ON api.access_request (created_at DESC) WHERE state = 'pending';

CREATE INDEX access_request_owner_idx ON api.access_request (user_id, created_at DESC);

CREATE TABLE api.approval (
user_id text NOT NULL,
scope text NOT NULL,
granted_by text NOT NULL,
granted_at timestamptz NOT NULL DEFAULT now(),
revoked_by text,
revoked_at timestamptz,
PRIMARY KEY (user_id, scope)
);

CREATE INDEX approval_live_idx ON api.approval (scope) WHERE revoked_at IS NULL;

ALTER TABLE api.token ADD COLUMN rotated_at timestamptz;
76 changes: 76 additions & 0 deletions db/migrations/0159_api_apps.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
CREATE TABLE api.app (
id bigserial PRIMARY KEY,
slug text NOT NULL UNIQUE,
name text NOT NULL,
blurb text NOT NULL,
owner_user_id text NOT NULL,
created_at timestamptz NOT NULL DEFAULT now(),
retired_at timestamptz
);

CREATE INDEX app_owner_idx ON api.app (owner_user_id);
CREATE INDEX app_live_idx ON api.app (name) WHERE retired_at IS NULL;

DROP TABLE api.approval;

CREATE TABLE api.approval (
app_id bigint NOT NULL REFERENCES api.app (id),
scope text NOT NULL,
granted_by text NOT NULL,
granted_at timestamptz NOT NULL DEFAULT now(),
revoked_by text,
revoked_at timestamptz,
PRIMARY KEY (app_id, scope)
);

CREATE INDEX approval_live_idx ON api.approval (scope) WHERE revoked_at IS NULL;

DROP TABLE api.consent;
DROP TABLE api.consent_log;

CREATE TABLE api.consent (
user_id text NOT NULL,
app_id bigint NOT NULL REFERENCES api.app (id),
capability text NOT NULL,
state text NOT NULL,
changed_at timestamptz NOT NULL DEFAULT now(),
changed_via text NOT NULL,
first_granted_at timestamptz,
PRIMARY KEY (user_id, app_id, capability),
CONSTRAINT consent_state CHECK (state IN ('granted', 'withheld'))
);

CREATE INDEX consent_granted_idx ON api.consent (app_id, capability)
WHERE state = 'granted';

CREATE TABLE api.consent_log (
id bigserial PRIMARY KEY,
user_id text NOT NULL,
app_id bigint NOT NULL REFERENCES api.app (id),
capability text NOT NULL,
state text NOT NULL,
via text NOT NULL,
at timestamptz NOT NULL DEFAULT now()
);

CREATE INDEX consent_log_at_idx ON api.consent_log (at DESC);

DELETE FROM api.request_log;
DELETE FROM api.token;

ALTER TABLE api.token ADD COLUMN app_id bigint NOT NULL REFERENCES api.app (id);

CREATE INDEX token_app_idx ON api.token (app_id);

DELETE FROM api.access_request;

ALTER TABLE api.access_request DROP COLUMN user_id;
ALTER TABLE api.access_request ADD COLUMN app_id bigint NOT NULL REFERENCES api.app (id);

DROP INDEX IF EXISTS api.access_request_one_open_idx;
DROP INDEX IF EXISTS api.access_request_owner_idx;

CREATE UNIQUE INDEX access_request_one_open_idx
ON api.access_request (app_id, scope) WHERE state = 'pending';

CREATE INDEX access_request_app_idx ON api.access_request (app_id, created_at DESC);
2 changes: 2 additions & 0 deletions db/migrations/0160_api_one_key_per_app.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
UPDATE api.setting SET value = 1, changed_at = now()
WHERE key = 'tokens_per_owner' AND value = 3;
32 changes: 32 additions & 0 deletions db/migrations/0161_api_approval_is_per_app.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
CREATE TABLE api.approval_per_app (
app_id bigint PRIMARY KEY REFERENCES api.app (id),
granted_by text NOT NULL,
granted_at timestamptz NOT NULL DEFAULT now(),
revoked_by text,
revoked_at timestamptz
);

INSERT INTO api.approval_per_app (app_id, granted_by, granted_at)
SELECT DISTINCT ON (app_id) app_id, granted_by, granted_at
FROM api.approval
WHERE revoked_at IS NULL
ORDER BY app_id, granted_at;

DROP TABLE api.approval;

ALTER TABLE api.approval_per_app RENAME TO approval;

CREATE INDEX approval_live_idx ON api.approval (app_id) WHERE revoked_at IS NULL;

DROP INDEX IF EXISTS api.access_request_one_open_idx;

DELETE FROM api.access_request a
WHERE EXISTS (
SELECT 1 FROM api.access_request b
WHERE b.app_id = a.app_id AND b.state = 'pending' AND b.id < a.id AND a.state = 'pending'
);

ALTER TABLE api.access_request DROP COLUMN scope;

CREATE UNIQUE INDEX access_request_one_open_idx
ON api.access_request (app_id) WHERE state = 'pending';
13 changes: 13 additions & 0 deletions db/migrations/0162_api_consent_can_be_for_every_app.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
ALTER TABLE api.consent DROP CONSTRAINT consent_pkey;

ALTER TABLE api.consent ALTER COLUMN app_id DROP NOT NULL;

CREATE UNIQUE INDEX consent_one_row_idx
ON api.consent (user_id, capability, coalesce(app_id, 0));

DROP INDEX IF EXISTS api.consent_granted_idx;

CREATE INDEX consent_granted_idx ON api.consent (capability, app_id)
WHERE state = 'granted';

ALTER TABLE api.consent_log ALTER COLUMN app_id DROP NOT NULL;
3 changes: 3 additions & 0 deletions db/migrations/0163_api_consent_has_a_key.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
ALTER TABLE api.consent ADD COLUMN id bigserial;

ALTER TABLE api.consent ADD CONSTRAINT consent_pkey PRIMARY KEY (id);
1 change: 1 addition & 0 deletions deploy/env/serve.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ FD_ENCRYPTION_SALT=
INTERNAL_PROXY_URL=
PROMETHEUS_BASE_URL=
SLACKSCAN_URL=
SLACK_CHANNEL_MANAGER_ROLE_ID=
PROXY_TOKEN_WEB=
PROXY_ALLOW_PLAINTEXT=
RAILS_MAX_THREADS=5
Expand Down
1 change: 1 addition & 0 deletions pipeline/bot/manifests/nemo.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ oauth_config:
user:
- admin
- admin.analytics:read
- admin.roles:read
- admin.conversations:read
- admin.teams:read
- admin.users:read
Expand Down
1 change: 0 additions & 1 deletion pipeline/bot/nemo/cards/people.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,6 @@ def picked(state):
return {
"user_ids": values.get(WHO, {}).get(WHO, {}).get("selected_users") or [],
"role": SUBJECT,
"detail": None,
}


Expand Down
Loading
Loading