Skip to content

Commit e178e55

Browse files
committed
Multiple changes.
* Bundled fleetspeak: make fleetspeak the default. * Sandboxing: move sandboxing options from `Platform:` context to `Target:` context. * Implement UI for recollecting VFS files. * Implement Store layer for recollecting VFS files.
1 parent ca301d5 commit e178e55

File tree

10 files changed

+343
-19
lines changed

10 files changed

+343
-19
lines changed

grr/core/install_data/etc/grr-server.yaml

Lines changed: 12 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,10 @@ Target:Darwin:
5858
Client.install_path: |
5959
/usr/local/lib/%(Client.name)/%(ClientRepacker.output_basename)
6060
61+
Client.unprivileged_user: nobody
62+
Client.unprivileged_group: nobody
63+
Client.use_filesystem_sandboxing: True
64+
6165
ClientBuilder.build_dest: "%(Client.name)-build"
6266

6367
ClientBuilder.build_root_dir: /Users/%(USER|env)/mac-build
@@ -121,6 +125,11 @@ Target:Linux:
121125
PyInstaller.distpath: |
122126
%(PyInstaller.dpkg_root)/debian/
123127
128+
Client.unprivileged_user: _%(Client.name)
129+
Client.unprivileged_group: _%(Client.name)
130+
Client.use_filesystem_sandboxing: True
131+
Client.use_memory_sandboxing: True
132+
124133
Target:LinuxRpm:
125134
ClientBuilder.output_extension: .rpm
126135

@@ -168,6 +177,9 @@ Target:Windows:
168177

169178
ClientBuilder.template_extension: .exe.zip
170179

180+
Client.use_filesystem_sandboxing: True
181+
Client.use_memory_sandboxing: True
182+
171183
Target:WindowsMsi:
172184
ClientBuilder.output_extension: .msi
173185

@@ -196,11 +208,6 @@ Platform:Darwin:
196208

197209
Client.platform: darwin
198210

199-
Global Install Context:
200-
Client.unprivileged_user: nobody
201-
Client.unprivileged_group: nobody
202-
Client.use_filesystem_sandboxing: True
203-
204211
Client Context:
205212
Logging.engines: stderr,file,syslog
206213

@@ -211,12 +218,6 @@ Platform:Linux:
211218

212219
Client.platform: linux
213220

214-
Global Install Context:
215-
Client.unprivileged_user: _%(Client.name)
216-
Client.unprivileged_group: _%(Client.name)
217-
Client.use_filesystem_sandboxing: True
218-
Client.use_memory_sandboxing: True
219-
220221
Client Context:
221222

222223
Logging.engines: stderr,file,syslog
@@ -244,10 +245,6 @@ Platform:Windows:
244245
- "%(install_path)"
245246
Client.grr_tempdir: "Temp"
246247

247-
Global Install Context:
248-
Client.use_filesystem_sandboxing: True
249-
Client.use_memory_sandboxing: True
250-
251248
Arch:amd64:
252249
ClientBuilder.vs_arch: x64
253250

grr/server/grr_response_server/bin/config_updater_util.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -342,11 +342,11 @@ def Prompt(self, config):
342342

343343
if self._IsFleetspeakPresent():
344344
self.use_fleetspeak = RetryBoolQuestion(
345-
"Use Fleetspeak (EXPERIMENTAL, next generation communication "
346-
"framework)?", False)
345+
"Use Fleetspeak (next generation communication "
346+
"framework)?", True)
347347
else:
348348
self.use_fleetspeak = False
349-
print("Fleetspeak (EXPERIMENTAL, optional, next generation "
349+
print("Fleetspeak (next generation "
350350
"communication framework) seems to be missing.")
351351
print("Skipping Fleetspeak configuration.\n")
352352

grr/server/grr_response_server/gui/ui/components/file_details/file_details.ng.html

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,12 @@ <h3 class="mat-h3">File details</h3>
1515
</tr>
1616
</table>
1717

18+
<button mat-stroked-button (click)="recollect()" [disabled]="isRecollecting$ | async" class="recollect">
19+
<mat-spinner *ngIf="isRecollecting$ | async; else enabledIcon" diameter="24"></mat-spinner>
20+
<ng-template #enabledIcon><mat-icon>refresh</mat-icon></ng-template>
21+
Recollect from client
22+
</button>
23+
1824
<pre
1925
class="monospace"
2026
><span *ngFor="let line of textContent$ | async; last as isLast">{{

grr/server/grr_response_server/gui/ui/components/file_details/file_details.scss

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,15 @@ pre span:before {
4242
width: 100%;
4343
}
4444

45+
.recollect {
46+
align-self: start;
47+
margin-left: 1em;
48+
49+
mat-spinner {
50+
display: inline-block;
51+
}
52+
}
53+
4554
.more-indicator {
4655
display: inline;
4756
background: rgba(0, 0, 0, 0.04);

grr/server/grr_response_server/gui/ui/components/file_details/file_details.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,8 @@ export class FileDetails implements OnDestroy {
4646
}),
4747
);
4848

49+
readonly isRecollecting$ = this.fileDetailsLocalStore.isRecollecting$;
50+
4951
constructor(
5052
private readonly selectedClientGlobalStore: SelectedClientGlobalStore,
5153
private readonly fileDetailsLocalStore: FileDetailsLocalStore,
@@ -65,4 +67,8 @@ export class FileDetails implements OnDestroy {
6567
loadMore() {
6668
this.fileDetailsLocalStore.fetchMoreContent(this.DEFAULT_PAGE_LENGTH);
6769
}
70+
71+
recollect() {
72+
this.fileDetailsLocalStore.recollectFile();
73+
}
6874
}

grr/server/grr_response_server/gui/ui/components/file_details/file_details_module.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import {NgModule} from '@angular/core';
22
import {MatButtonModule} from '@angular/material/button';
33
import {MatIconModule} from '@angular/material/icon';
4+
import {MatProgressSpinnerModule} from '@angular/material/progress-spinner';
45
import {MatTooltipModule} from '@angular/material/tooltip';
56
import {BrowserAnimationsModule} from '@angular/platform-browser/animations';
67
import {RouterModule} from '@angular/router';
@@ -19,6 +20,7 @@ import {FileDetailsRoutingModule} from './routing';
1920
MatButtonModule,
2021
MatIconModule,
2122
MatTooltipModule,
23+
MatProgressSpinnerModule,
2224

2325
FileDetailsRoutingModule,
2426
HumanReadableSizeModule,

grr/server/grr_response_server/gui/ui/components/file_details/file_details_test.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,4 +137,25 @@ describe('FileDetails Component', () => {
137137
expect(fileDetailsLocalStore.fetchMoreContent)
138138
.toHaveBeenCalledTimes(previousCalls + 1);
139139
});
140+
141+
it('reloads content on "recollect" click', () => {
142+
const fixture = TestBed.createComponent(FileDetails);
143+
fixture.detectChanges();
144+
145+
fileDetailsLocalStore.mockedObservables.textContent$.next('hello');
146+
fixture.detectChanges();
147+
148+
expect(fileDetailsLocalStore.recollectFile).not.toHaveBeenCalled();
149+
150+
const recollectButton = fixture.debugElement.query(By.css('.recollect'));
151+
recollectButton.triggerEventHandler('click', new MouseEvent('click'));
152+
fixture.detectChanges();
153+
154+
expect(fileDetailsLocalStore.recollectFile).toHaveBeenCalledOnceWith();
155+
156+
fileDetailsLocalStore.mockedObservables.textContent$.next('hellonew');
157+
fixture.detectChanges();
158+
expect(fixture.debugElement.nativeElement.textContent)
159+
.toContain('hellonew');
160+
});
140161
});
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
/** Test helpers. */
2+
// tslint:disable:no-any
3+
4+
import {TestBed} from '@angular/core/testing';
5+
import {Observable, Subject} from 'rxjs';
6+
7+
import {HttpApiService} from './http_api_service';
8+
9+
type Func = (...args: any[]) => any;
10+
11+
type MockService<T> = {
12+
[K in keyof T]: T[K] extends Function ? jasmine.Spy&T[K] : T[K];
13+
}&{
14+
readonly mockedObservables: {
15+
[K in keyof T]: T[K] extends Func ?
16+
ReturnType<T[K]> extends Observable<infer V>? Subject<V>: never :
17+
never;
18+
}
19+
};
20+
21+
/** HttpApiService with Spy properties and mocked Observable return values. */
22+
export declare interface HttpApiServiceMock extends
23+
MockService<HttpApiService> {}
24+
25+
/**
26+
* Mocks a HttpApiService, replacing methods with jasmine spies that return
27+
* Observables from `httpApiServiceMock.mockedObservables`.
28+
*/
29+
export function mockHttpApiService(): HttpApiServiceMock {
30+
const mockHttpClient = {
31+
get: jasmine.createSpy('get').and.callFake(() => new Subject()),
32+
post: jasmine.createSpy('post').and.callFake(() => new Subject()),
33+
};
34+
const service: any = new HttpApiService(mockHttpClient as any);
35+
const mockedObservables: any = {};
36+
37+
const properties = Object.getOwnPropertyNames(HttpApiService.prototype)
38+
.filter(key => service[key] instanceof Function);
39+
40+
for (const property of properties) {
41+
mockedObservables[property] = new Subject();
42+
service[property] = jasmine.createSpy(property).and.callFake(
43+
() => mockedObservables[property].asObservable());
44+
}
45+
46+
service.mockedObservables = mockedObservables;
47+
return service;
48+
}
49+
50+
/** Injects the MockStore for the given Store class. */
51+
export function injectHttpApiServiceMock(): HttpApiServiceMock {
52+
const mock = TestBed.inject(HttpApiService) as unknown as HttpApiServiceMock;
53+
54+
if (!mock.mockedObservables) {
55+
const val = JSON.stringify(mock).slice(0, 100);
56+
throw new Error(`TestBed.inject(HttpApiService) returned ${
57+
val}, which does not look like HttpApiServiceMock. Did you register the HttpApiService providers?`);
58+
}
59+
60+
return mock;
61+
}

grr/server/grr_response_server/gui/ui/store/file_details_local_store.ts

Lines changed: 77 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import {Injectable} from '@angular/core';
22
import {ComponentStore} from '@ngrx/component-store';
33
import {HttpApiService} from '@app/lib/api/http_api_service';
4-
import {Observable} from 'rxjs';
4+
import {Observable, of} from 'rxjs';
55
import {map, switchMap, tap, withLatestFrom} from 'rxjs/operators';
66

77
import {ApiGetFileTextArgsEncoding, ApiGetFileTextResult, PathSpecPathType} from '../lib/api/api_interfaces';
@@ -11,9 +11,11 @@ import {assertKeyNonNull} from '../lib/preconditions';
1111

1212
interface State {
1313
readonly file?: FileIdentifier;
14+
readonly fetchContentLength?: bigint;
1415
readonly textContent?: string;
1516
readonly totalSize?: bigint;
1617
readonly details?: File;
18+
readonly isRecollecting?: boolean;
1719
}
1820

1921
interface FileIdentifier {
@@ -47,9 +49,15 @@ export class FileDetailsLocalStore {
4749

4850
readonly hasMore$ = this.store.hasMore$;
4951

52+
readonly isRecollecting$ = this.store.isRecollecting$;
53+
5054
fetchMoreContent(length: bigint) {
5155
this.store.fetchMoreContent(length);
5256
}
57+
58+
recollectFile() {
59+
this.store.recollectFile();
60+
}
5361
}
5462

5563
class FileDetailsComponentStore extends ComponentStore<State> {
@@ -87,9 +95,13 @@ class FileDetailsComponentStore extends ComponentStore<State> {
8795

8896
readonly fetchMoreContent = this.effect<bigint>(
8997
obs$ => obs$.pipe(
98+
tap(fetchLength => {
99+
this.increaseFetchContentLength(fetchLength);
100+
}),
90101
withLatestFrom(this.state$),
91102
switchMap(([fetchLength, state]) => {
92103
assertKeyNonNull(state, 'file');
104+
93105
return this.httpApiService.getFileText(
94106
state.file.clientId, state.file.pathType, state.file.path, {
95107
encoding: ENCODING,
@@ -102,13 +114,77 @@ class FileDetailsComponentStore extends ComponentStore<State> {
102114
}),
103115
));
104116

117+
readonly isRecollecting$ =
118+
this.select(state => state.isRecollecting)
119+
.pipe(
120+
map((isRecollecting) => isRecollecting ?? false),
121+
);
122+
123+
readonly setIsRecollecting =
124+
this.updater<boolean>((state, isRecollecting) => ({
125+
...state,
126+
isRecollecting,
127+
}));
128+
129+
readonly recollectFile = this.effect<void>(
130+
obs$ => obs$.pipe(
131+
tap(() => {
132+
this.setIsRecollecting(true);
133+
}),
134+
withLatestFrom(this.state$),
135+
switchMap(([param, state]) => {
136+
assertKeyNonNull(state, 'file');
137+
return this.httpApiService
138+
.updateVfsFileContent(
139+
state.file.clientId, state.file.pathType, state.file.path)
140+
.pipe(
141+
map(details => ({details, state})),
142+
);
143+
}),
144+
switchMap(({details, state}) => {
145+
const content$: Observable<ApiGetFileTextResult|null> =
146+
state.fetchContentLength ?
147+
this.httpApiService.getFileText(
148+
state.file.clientId, state.file.pathType, state.file.path, {
149+
encoding: ENCODING,
150+
offset: 0,
151+
length: state.fetchContentLength,
152+
}) :
153+
of(null);
154+
return content$.pipe(
155+
map(content => ({details, content})),
156+
);
157+
}),
158+
tap(({details, content}) => {
159+
this.setIsRecollecting(false);
160+
this.updateDetails(translateFile(details));
161+
if (content !== null) {
162+
this.setTextContent(content);
163+
}
164+
}),
165+
));
166+
105167
private readonly appendTextContent = this.updater<ApiGetFileTextResult>(
106168
(state, result) => ({
107169
...state,
108170
textContent: (state.textContent ?? '') + (result.content ?? ''),
109171
totalSize: BigInt(result.totalSize ?? 0),
110172
}));
111173

174+
175+
private readonly setTextContent = this.updater<ApiGetFileTextResult>(
176+
(state, result) => ({
177+
...state,
178+
textContent: result.content ?? '',
179+
totalSize: BigInt(result.totalSize ?? 0),
180+
}));
181+
182+
private readonly increaseFetchContentLength = this.updater<bigint>(
183+
(state, length) => ({
184+
...state,
185+
fetchContentLength: (state.fetchContentLength ?? BigInt(0)) + length,
186+
}));
187+
112188
private readonly updateDetails =
113189
this.updater<File>((state, details) => ({...state, details}));
114190
}

0 commit comments

Comments
 (0)