feat(settings): hub auth-policy panel — T4/T5 security parity in the GUI
Some checks failed
Security / Security check (push) Failing after 1s
Some checks failed
Security / Security check (push) Failing after 1s
Settings → Security now shows the hub's effective auth policy via the new read-only AuthStatus RPC: active token validator (static / jwt-rs256 with issuer, audience, JWKS source), anonymous-access warning, per-token cards with scope grants, env-var presence and rate limits, plus a localized admin-denied story for non-admin tokens. Live-reloads on endpoint change. Also fixes a batch of fai→chain rename leftovers this panel's verification uncovered: hub_auth_token.dart and registry_token.dart read/wrote ~/.fai/ while the hub reads ~/.chain/ (stored registry tokens never reached the hub), today_story_loader + tools/today used ~/.fai/today, chain_log legacy ~/.fai/logs migration removed per the no-legacy-recognisers decision, and UI strings still advertised the retired .fai bundle extension. Includes 5 widget tests for the panel, an integration-test screenshot harness (auth_policy_shots_test.dart, guide-shots style), and DE+EN l10n. flutter analyze clean, 58 tests green. Signed-off-by: flemming-it <stefan.a.flemming@googlemail.com>
This commit is contained in:
parent
c6da5025ce
commit
efaa089454
18 changed files with 1208 additions and 74 deletions
187
integration_test/auth_policy_shots_test.dart
Normal file
187
integration_test/auth_policy_shots_test.dart
Normal file
|
|
@ -0,0 +1,187 @@
|
|||
// Screenshot proof for Settings → Security → hub auth policy
|
||||
// (the T4/T5 parity panel): boots a hermetic hub WITH token auth
|
||||
// (fixture config via $CHAIN_CONFIG, secrets via env), connects
|
||||
// as the admin token, opens the settings dialog on the Security
|
||||
// category and captures the rendered policy — validator, token
|
||||
// cards with scope grants, the env-not-set rotation warning.
|
||||
//
|
||||
// Like guide_shots_test.dart this renders through a
|
||||
// RepaintBoundary, so it works headed on macOS with plain
|
||||
// `flutter test` and does not depend on the screen being
|
||||
// unlocked. Theme / locale come from the same env knobs:
|
||||
//
|
||||
// GUIDE_SHOTS_THEME=light GUIDE_SHOTS_LOCALE=en \
|
||||
// flutter test integration_test/auth_policy_shots_test.dart -d macos
|
||||
//
|
||||
// Output lands in build/guide-shots/ unless GUIDE_SHOTS_OUT is set.
|
||||
|
||||
import 'dart:io';
|
||||
import 'dart:ui' as ui;
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/rendering.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
import 'package:chain_client_sdk/chain_client_sdk.dart' show HubEndpoint;
|
||||
import 'package:chain_studio/data/hub.dart';
|
||||
import 'package:chain_studio/main.dart';
|
||||
import 'package:chain_studio/widgets/widgets.dart';
|
||||
|
||||
import '../test/integration/hub_fixture.dart';
|
||||
|
||||
final GlobalKey _shotKey = GlobalKey();
|
||||
|
||||
const _adminSecret = 'shot-admin-secret';
|
||||
|
||||
/// The fixture hub's auth policy. studio-admin is the caller,
|
||||
/// digiscout-prod shows a fine-grained T5 scope + rate limit.
|
||||
/// ci-reader is appended AFTER boot (the hub refuses to start on
|
||||
/// an unset token_env, but AuthStatus re-reads the config live) —
|
||||
/// that's the rotation footgun the panel exists to surface.
|
||||
const _configYaml = '''
|
||||
auth:
|
||||
tokens:
|
||||
- name: studio-admin
|
||||
token_env: CHAIN_TOKEN_STUDIO_ADMIN
|
||||
scopes: [admin, read, execute, install]
|
||||
- name: digiscout-prod
|
||||
token_env: CHAIN_TOKEN_DIGISCOUT
|
||||
scopes: [read, "execute:llm.*"]
|
||||
rate_limit_per_minute: 120
|
||||
''';
|
||||
|
||||
const _ciReaderYaml = '''
|
||||
- name: ci-reader
|
||||
token_env: CHAIN_TOKEN_CI
|
||||
scopes: [read]
|
||||
''';
|
||||
|
||||
Future<void> _pumpFrames(WidgetTester tester, [int frames = 20]) async {
|
||||
for (var i = 0; i < frames; i++) {
|
||||
await tester.pump(const Duration(milliseconds: 100));
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _pumpUntil(
|
||||
WidgetTester tester,
|
||||
Finder finder, {
|
||||
int maxFrames = 100,
|
||||
}) async {
|
||||
for (var i = 0; i < maxFrames; i++) {
|
||||
if (finder.evaluate().isNotEmpty) return;
|
||||
await tester.pump(const Duration(milliseconds: 100));
|
||||
}
|
||||
}
|
||||
|
||||
String get _outDir =>
|
||||
Platform.environment['GUIDE_SHOTS_OUT'] ?? 'build/guide-shots';
|
||||
|
||||
ThemeModeValue get _theme =>
|
||||
Platform.environment['GUIDE_SHOTS_THEME'] == 'light'
|
||||
? ThemeModeValue.light
|
||||
: ThemeModeValue.dark;
|
||||
|
||||
Locale get _locale => Locale(
|
||||
Platform.environment['GUIDE_SHOTS_LOCALE'] == 'en' ? 'en' : 'de',
|
||||
);
|
||||
|
||||
Future<void> _shot(WidgetTester tester, String name) async {
|
||||
await tester.pump(const Duration(milliseconds: 120));
|
||||
await tester.pump(const Duration(milliseconds: 120));
|
||||
final boundary =
|
||||
_shotKey.currentContext!.findRenderObject() as RenderRepaintBoundary;
|
||||
final image = await boundary.toImage(pixelRatio: 2.0);
|
||||
final bytes = await image.toByteData(format: ui.ImageByteFormat.png);
|
||||
image.dispose();
|
||||
final file = File('$_outDir/$name.png');
|
||||
file.parent.createSync(recursive: true);
|
||||
file.writeAsBytesSync(bytes!.buffer.asUint8List());
|
||||
// ignore: avoid_print
|
||||
print('guide-shot: ${file.path}');
|
||||
}
|
||||
|
||||
void main() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
testWidgets('capture the Settings → Security auth-policy panel',
|
||||
(tester) async {
|
||||
final fixture = await HubFixture.start(
|
||||
skipIfBinaryMissing: false,
|
||||
configYaml: _configYaml,
|
||||
extraEnvironment: {
|
||||
'CHAIN_TOKEN_STUDIO_ADMIN': _adminSecret,
|
||||
'CHAIN_TOKEN_DIGISCOUT': 'shot-digiscout-secret',
|
||||
},
|
||||
authToken: _adminSecret,
|
||||
);
|
||||
addTearDown(fixture!.dispose);
|
||||
|
||||
// Rotation scenario: a token added to the config after the
|
||||
// daemon started, its env var not exported yet → the panel
|
||||
// must flag it (env_set=false).
|
||||
File('${fixture.tempDir.path}/config.yaml')
|
||||
.writeAsStringSync(_configYaml + _ciReaderYaml);
|
||||
|
||||
// Hermetic prefs: never read or write the operator's real ones.
|
||||
SharedPreferences.setMockInitialValues({});
|
||||
|
||||
await tester.pumpWidget(
|
||||
RepaintBoundary(
|
||||
key: _shotKey,
|
||||
child: StudioApp(
|
||||
initialThemeMode: _theme,
|
||||
initialLocale: _locale,
|
||||
),
|
||||
),
|
||||
);
|
||||
await HubService.instance.reconnect(
|
||||
HubEndpoint(host: '127.0.0.1', port: fixture.port),
|
||||
authToken: _adminSecret,
|
||||
persist: false,
|
||||
);
|
||||
await _pumpFrames(tester, 30);
|
||||
|
||||
// Settings gear (sidebar) → Security category.
|
||||
await tester.tap(find.byIcon(Icons.settings_outlined).first);
|
||||
await _pumpFrames(tester);
|
||||
final securityTab = find.text(
|
||||
_locale.languageCode == 'en' ? 'Security' : 'Sicherheit',
|
||||
);
|
||||
await _pumpUntil(tester, securityTab);
|
||||
await tester.tap(securityTab.first);
|
||||
await _pumpFrames(tester);
|
||||
|
||||
// The policy loads from the hub asynchronously; the ci-reader
|
||||
// card is the last to prove the full token list arrived.
|
||||
await _pumpUntil(tester, find.textContaining('ci-reader'));
|
||||
|
||||
// Bring the panel into view — it sits below the registry +
|
||||
// hub-token sections in the Security category's scroll area.
|
||||
if (find.textContaining('ci-reader').evaluate().isNotEmpty) {
|
||||
await tester.scrollUntilVisible(
|
||||
find.textContaining('ci-reader').first,
|
||||
120,
|
||||
scrollable: find
|
||||
.descendant(
|
||||
of: find.byType(ChainSettingsDialog),
|
||||
matching: find.byType(Scrollable),
|
||||
)
|
||||
.first,
|
||||
);
|
||||
await _pumpFrames(tester, 5);
|
||||
}
|
||||
|
||||
// Capture before asserting so a failing run still leaves a
|
||||
// diagnosable image behind.
|
||||
final suffix =
|
||||
'${_theme == ThemeModeValue.light ? 'hell' : 'dunkel'}-${_locale.languageCode}';
|
||||
await _shot(tester, 'einstellungen-sicherheit-$suffix');
|
||||
|
||||
expect(find.textContaining('studio-admin'), findsWidgets);
|
||||
expect(find.textContaining('digiscout-prod'), findsWidgets);
|
||||
expect(find.textContaining('execute:llm.*'), findsWidgets);
|
||||
expect(find.textContaining('ci-reader'), findsWidgets);
|
||||
});
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue