Skip to content

Commit a77d969

Browse files
Show controls as read only based on tenant permissions (opensearch-project#1472) (opensearch-project#1670)
Signed-off-by: Kajetan Nobel <k.nobel@routegroup.pl> Signed-off-by: Kajetan Nobel <kajetan.nobel@eliatra.com> Co-authored-by: Stephen Crawford <65832608+scrawfor99@users.noreply.github.com> Co-authored-by: Darshit Chanpura <35282393+DarshitChanpura@users.noreply.github.com> Co-authored-by: Peter Nied <peternied@hotmail.com> Co-authored-by: Peter Nied <petern@amazon.com> (cherry picked from commit cfc83dd94eea02b5738bf607dd9866308814f2fc) Co-authored-by: jakubp-eliatra <126599757+jakubp-eliatra@users.noreply.github.com>
1 parent a64fc52 commit a77d969

File tree

4 files changed

+349
-10
lines changed

4 files changed

+349
-10
lines changed

server/auth/types/authentication_type.ts

+5-5
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,11 @@ export interface IAuthenticationType {
3737
type: string;
3838
authHandler: AuthenticationHandler;
3939
init: () => Promise<void>;
40+
requestIncludesAuthInfo(request: OpenSearchDashboardsRequest): boolean;
41+
buildAuthHeaderFromCookie(
42+
cookie: SecuritySessionCookie,
43+
request: OpenSearchDashboardsRequest
44+
): any;
4045
}
4146

4247
export type IAuthHandlerConstructor = new (
@@ -267,7 +272,6 @@ export abstract class AuthenticationType implements IAuthenticationType {
267272
}
268273

269274
// abstract functions for concrete auth types to implement
270-
public abstract requestIncludesAuthInfo(request: OpenSearchDashboardsRequest): boolean;
271275
public abstract getAdditionalAuthHeader(request: OpenSearchDashboardsRequest): Promise<any>;
272276
public abstract getCookie(
273277
request: OpenSearchDashboardsRequest,
@@ -282,9 +286,5 @@ export abstract class AuthenticationType implements IAuthenticationType {
282286
response: LifecycleResponseFactory,
283287
toolkit: AuthToolkit
284288
): IOpenSearchDashboardsResponse | AuthResult;
285-
public abstract buildAuthHeaderFromCookie(
286-
cookie: SecuritySessionCookie,
287-
request: OpenSearchDashboardsRequest
288-
): any;
289289
public abstract init(): Promise<void>;
290290
}

server/plugin.ts

+13-5
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@
1515

1616
import { first } from 'rxjs/operators';
1717
import { Observable } from 'rxjs';
18-
import { ResponseObject } from '@hapi/hapi';
1918
import {
2019
PluginInitializerContext,
2120
CoreSetup,
@@ -39,16 +38,14 @@ import {
3938
ISavedObjectTypeRegistry,
4039
} from '../../../src/core/server/saved_objects';
4140
import { setupIndexTemplate, migrateTenantIndices } from './multitenancy/tenant_index';
42-
import {
43-
IAuthenticationType,
44-
OpenSearchDashboardsAuthState,
45-
} from './auth/types/authentication_type';
41+
import { IAuthenticationType } from './auth/types/authentication_type';
4642
import { getAuthenticationHandler } from './auth/auth_handler_factory';
4743
import { setupMultitenantRoutes } from './multitenancy/routes';
4844
import { defineAuthTypeRoutes } from './routes/auth_type_routes';
4945
import { createMigrationOpenSearchClient } from '../../../src/core/server/saved_objects/migrations/core';
5046
import { SecuritySavedObjectsClientWrapper } from './saved_objects/saved_objects_wrapper';
5147
import { addTenantParameterToResolvedShortLink } from './multitenancy/tenant_resolver';
48+
import { ReadonlyService } from './readonly/readonly_service';
5249

5350
export interface SecurityPluginRequestContext {
5451
logger: Logger;
@@ -138,6 +135,7 @@ export class SecurityPlugin implements Plugin<SecurityPluginSetup, SecurityPlugi
138135
// Register server side APIs
139136
defineRoutes(router);
140137
defineAuthTypeRoutes(router, config);
138+
141139
// set up multi-tenent routes
142140
if (config.multitenancy?.enabled) {
143141
setupMultitenantRoutes(router, securitySessionStorageFactory, this.securityClient);
@@ -151,6 +149,16 @@ export class SecurityPlugin implements Plugin<SecurityPluginSetup, SecurityPlugi
151149
);
152150
}
153151

152+
const service = new ReadonlyService(
153+
this.logger,
154+
this.securityClient,
155+
auth,
156+
securitySessionStorageFactory,
157+
config
158+
);
159+
160+
core.security.registerReadonlyService(service);
161+
154162
return {
155163
config$,
156164
securityConfigClient: esClient,
+212
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,212 @@
1+
/*
2+
* Copyright OpenSearch Contributors
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License").
5+
* You may not use this file except in compliance with the License.
6+
* A copy of the License is located at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* or in the "license" file accompanying this file. This file is distributed
11+
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
12+
* express or implied. See the License for the specific language governing
13+
* permissions and limitations under the License.
14+
*/
15+
16+
import { loggerMock } from '@osd/logging/target/mocks';
17+
import { httpServerMock, sessionStorageMock } from '../../../../src/core/server/mocks';
18+
import { ILegacyClusterClient } from '../../../../src/core/server/opensearch/legacy/cluster_client';
19+
import { PRIVATE_TENANT_SYMBOL } from '../../common/index';
20+
import { OpenSearchAuthInfo } from '../auth/types/authentication_type';
21+
import { BasicAuthentication } from '../auth/types/index';
22+
import { SecurityClient } from '../backend/opensearch_security_client';
23+
import { SecurityPluginConfigType } from '../index';
24+
import { SecuritySessionCookie } from '../session/security_cookie';
25+
import { ReadonlyService } from './readonly_service';
26+
27+
jest.mock('../auth/types/basic/basic_auth');
28+
29+
const mockCookie = (data: Partial<SecuritySessionCookie> = {}): SecuritySessionCookie =>
30+
Object.assign(
31+
{
32+
username: 'test',
33+
credentials: {
34+
authHeaderValue: 'Basic cmVhZG9ubHk6Z2FzZGN4ejRRIQ==',
35+
},
36+
authType: 'basicauth',
37+
isAnonymousAuth: false,
38+
tenant: '__user__',
39+
},
40+
data
41+
);
42+
43+
const mockEsClient = (): jest.Mocked<ILegacyClusterClient> => {
44+
return {
45+
callAsInternalUser: jest.fn(),
46+
asScoped: jest.fn(),
47+
};
48+
};
49+
50+
const mockAuthInfo = (data: Partial<OpenSearchAuthInfo> = {}): OpenSearchAuthInfo =>
51+
Object.assign(
52+
{
53+
user: '',
54+
user_name: 'admin',
55+
user_requested_tenant: PRIVATE_TENANT_SYMBOL,
56+
remote_address: '127.0.0.1',
57+
backend_roles: ['admin'],
58+
custom_attribute_names: [],
59+
roles: ['own_index', 'all_access'],
60+
tenants: {
61+
admin_tenant: true,
62+
admin: true,
63+
},
64+
principal: null,
65+
peer_certificates: '0',
66+
sso_logout_url: null,
67+
},
68+
data
69+
);
70+
71+
const mockDashboardsInfo = (data = {}) =>
72+
Object.assign(
73+
{
74+
user_name: 'admin',
75+
multitenancy_enabled: true,
76+
},
77+
data
78+
);
79+
80+
const getService = (
81+
cookie: SecuritySessionCookie = mockCookie(),
82+
authInfo: OpenSearchAuthInfo = mockAuthInfo(),
83+
dashboardsInfo = mockDashboardsInfo()
84+
) => {
85+
const logger = loggerMock.create();
86+
87+
const securityClient = new SecurityClient(mockEsClient());
88+
securityClient.authinfo = jest.fn().mockReturnValue(authInfo);
89+
securityClient.dashboardsinfo = jest.fn().mockReturnValue(dashboardsInfo);
90+
91+
// @ts-ignore mock auth
92+
const auth = new BasicAuthentication();
93+
auth.requestIncludesAuthInfo = jest.fn().mockReturnValue(true);
94+
95+
const securitySessionStorageFactory = sessionStorageMock.createFactory<SecuritySessionCookie>();
96+
securitySessionStorageFactory.asScoped = jest.fn().mockReturnValue({
97+
get: jest.fn().mockResolvedValue(cookie),
98+
});
99+
100+
const config = {
101+
multitenancy: {
102+
enabled: true,
103+
},
104+
} as SecurityPluginConfigType;
105+
106+
return new ReadonlyService(logger, securityClient, auth, securitySessionStorageFactory, config);
107+
};
108+
109+
describe('checks isAnonymousPage', () => {
110+
const service = getService();
111+
112+
it.each([
113+
// Missing referer header
114+
[
115+
{
116+
path: '/api/core/capabilities',
117+
headers: {},
118+
auth: {
119+
isAuthenticated: false,
120+
mode: 'optional',
121+
},
122+
},
123+
false,
124+
],
125+
// Referer with not anynoumous page
126+
[
127+
{
128+
headers: {
129+
referer: 'https://localhost/app/management/opensearch-dashboards/indexPatterns',
130+
},
131+
},
132+
false,
133+
],
134+
// Referer with anynoumous page
135+
[
136+
{
137+
path: '/app/login',
138+
headers: {
139+
referer: 'https://localhost/app/login',
140+
},
141+
routeAuthRequired: false,
142+
},
143+
true,
144+
],
145+
])('%j returns result %s', (requestData, expectedResult) => {
146+
const request = httpServerMock.createOpenSearchDashboardsRequest(requestData);
147+
expect(service.isAnonymousPage(request)).toEqual(expectedResult);
148+
});
149+
});
150+
151+
describe('checks isReadOnlyTenant', () => {
152+
const service = getService();
153+
154+
it.each([
155+
// returns false with private global tenant
156+
[mockAuthInfo({ user_requested_tenant: PRIVATE_TENANT_SYMBOL }), false],
157+
// returns false when has requested tenant but it's read and write
158+
[
159+
mockAuthInfo({
160+
user_requested_tenant: 'readonly_tenant',
161+
tenants: {
162+
readonly_tenant: true,
163+
},
164+
}),
165+
false,
166+
],
167+
// returns true when has requested tenant and it's read only
168+
[
169+
mockAuthInfo({
170+
user_requested_tenant: 'readonly_tenant',
171+
tenants: {
172+
readonly_tenant: false,
173+
},
174+
}),
175+
true,
176+
],
177+
])('%j returns result %s', (authInfo, expectedResult) => {
178+
expect(service.isReadOnlyTenant(authInfo)).toBe(expectedResult);
179+
});
180+
});
181+
182+
describe('checks isReadonly', () => {
183+
it('calls isAnonymousPage', async () => {
184+
const service = getService();
185+
service.isAnonymousPage = jest.fn(() => true);
186+
await service.isReadonly(httpServerMock.createOpenSearchDashboardsRequest());
187+
expect(service.isAnonymousPage).toBeCalled();
188+
});
189+
it('calls isReadOnlyTenant with correct authinfo', async () => {
190+
const cookie = mockCookie({ tenant: 'readonly_tenant' });
191+
const authInfo = mockAuthInfo({
192+
user_requested_tenant: 'readonly_tenant',
193+
tenants: {
194+
readonly_tenant: false,
195+
},
196+
});
197+
198+
const service = getService(cookie, authInfo);
199+
service.isAnonymousPage = jest.fn(() => false);
200+
201+
const result = await service.isReadonly(httpServerMock.createOpenSearchDashboardsRequest());
202+
expect(result).toBeTruthy();
203+
});
204+
it('calls dashboardInfo and checks if multitenancy is enabled', async () => {
205+
const dashboardsInfo = mockDashboardsInfo({ multitenancy_enabled: false });
206+
const service = getService(mockCookie(), mockAuthInfo(), dashboardsInfo);
207+
service.isAnonymousPage = jest.fn(() => false);
208+
209+
const result = await service.isReadonly(httpServerMock.createOpenSearchDashboardsRequest());
210+
expect(result).toBeFalsy();
211+
});
212+
});

0 commit comments

Comments
 (0)