Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(credentials): implement form with matchExpression #481

Merged
merged 22 commits into from
Jul 14, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
66b6ff7
fix(credentials): use new API v2.2 credentials format
andrewazores Jul 7, 2022
bcc7964
chore(credentials): extract to subdirectory
andrewazores Jul 7, 2022
6b289fe
feat(jmx): remove cached credentials, use backend storage only
andrewazores Jul 11, 2022
7782e2c
feat(auth): add link from auth modal to /security
andrewazores Jul 11, 2022
9b697e8
feat(credentials): user can enter matchExpression for credentials
andrewazores Jul 11, 2022
7470580
update test
andrewazores Jul 11, 2022
0136296
fixup! feat(auth): add link from auth modal to /security
andrewazores Jul 11, 2022
d3e4fa4
addSubscription to ensure modals drop API requests when closed
andrewazores Jul 13, 2022
5fa6da7
avoid deprecated EnterKeyCode
andrewazores Jul 13, 2022
d89c3bb
reorder modal components for better layout
andrewazores Jul 13, 2022
ec91533
correct description
andrewazores Jul 13, 2022
e73d88b
evaluator hint inline or as tooltip
andrewazores Jul 13, 2022
99f5147
remove reference to unimplemented feature
andrewazores Jul 13, 2022
55fbba6
fix typo
andrewazores Jul 13, 2022
476ea3c
add API request expectations
andrewazores Jul 13, 2022
3942f4d
ensure autofocus starts in matchExpression field
andrewazores Jul 13, 2022
6764d2c
use notification to update state
andrewazores Jul 13, 2022
5bcb530
mergeMap subscriptions
andrewazores Jul 13, 2022
f0ed45a
map out connectUrl
andrewazores Jul 13, 2022
8554671
map out matchExpression
andrewazores Jul 13, 2022
84a06aa
add space between label and tooltip icon
andrewazores Jul 13, 2022
702741d
update snapshot
andrewazores Jul 13, 2022
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
47 changes: 34 additions & 13 deletions src/app/AppLayout/AuthModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,10 +36,13 @@
* SOFTWARE.
*/
import * as React from 'react';
import { Modal, ModalVariant } from '@patternfly/react-core';
import { Modal, ModalVariant, Text } from '@patternfly/react-core';
import { Link } from 'react-router-dom';
import { JmxAuthForm } from './JmxAuthForm';
import { ServiceContext } from '@app/Shared/Services/Services';
import { first } from 'rxjs';
import { filter, first, map, mergeMap } from 'rxjs';
import { NO_TARGET } from '@app/Shared/Services/Target.service';
import { useSubscriptions } from '@app/utils/useSubscriptions';

export interface AuthModalProps {
visible: boolean;
Expand All @@ -49,26 +52,44 @@ export interface AuthModalProps {

export const AuthModal: React.FunctionComponent<AuthModalProps> = (props) => {
const context = React.useContext(ServiceContext);
const addSubscription = useSubscriptions();

const handleDismiss = () => {
props.onDismiss();
};

const onSave = (username: string, password: string) => {
context.api.postTargetCredentials(username, password).pipe(first()).subscribe();
props.onSave();
};
const onSave = React.useCallback((username: string, password: string): Promise<void> => {
return new Promise((resolve, reject) => {
addSubscription(
context.target.target().pipe(
first(),
filter(target => target !== NO_TARGET),
map(target => target.connectUrl),
map(connectUrl => `target.connectUrl == "${connectUrl}"`),
mergeMap(matchExpression => context.api.postCredentials(matchExpression, username, password))
).subscribe(result => {
if (result) {
props.onSave();
resolve();
} else {
reject();
}
})
);
});
}, [context, context.target, context.api, props.onSave]);

return (
<Modal
isOpen={props.visible}
variant={ModalVariant.large}
showClose={true}
onClose={handleDismiss}
onClose={props.onDismiss}
title="Authentication Required"
description="This target JVM requires authentication. The credentials you provide here will be passed from Cryostat to the target when establishing JMX connections."
description={
<Text>
This target JVM requires authentication. The credentials you provide here will be passed from Cryostat to the target when establishing JMX connections.
Enter credentials specific to this target, or go to <Link onClick={props.onDismiss} to="/security">Security</Link> to add a credential matching multiple targets.
</Text>
}
>
<JmxAuthForm onSave={onSave} onDismiss={handleDismiss} />
<JmxAuthForm onSave={onSave} onDismiss={props.onDismiss} focus={true} />
</Modal>
);
};
42 changes: 18 additions & 24 deletions src/app/AppLayout/JmxAuthForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,52 +36,46 @@
* SOFTWARE.
*/
import * as React from 'react';
import { first } from 'rxjs/operators';
import { ActionGroup, Button, Form, FormGroup, Modal, ModalVariant, TextInput } from '@patternfly/react-core';
import { ActionGroup, Button, Form, FormGroup, TextInput } from '@patternfly/react-core';
import { ServiceContext } from '@app/Shared/Services/Services';

export interface JmxAuthFormProps {
onDismiss: () => void;
onSave: (username: string, password: string) => void;
onSave: (username: string, password: string) => Promise<void>;
focus?: boolean;
}

const EnterKeyCode = 13;

export const JmxAuthForm: React.FunctionComponent<JmxAuthFormProps> = (props) => {
const context = React.useContext(ServiceContext);
const [username, setUsername] = React.useState('');
const [password, setPassword] = React.useState('');

const clear = () => {
const clear = React.useCallback(() => {
setUsername('');
setPassword('');
};
}, [setUsername, setPassword]);

const handleSave = () => {
context.target
.target()
.pipe(first())
.subscribe((target) => {
context.target.setCredentials(target.connectUrl, `${username}:${password}`);
context.target.setAuthRetry();
clear();
props.onSave(username, password);
});
};
const handleSave = React.useCallback(() => {
props.onSave(username, password).then(() => {
clear();
context.target.setAuthRetry();
});
}, [context, context.target, clear, props.onSave, username, password]);

const handleDismiss = () => {
const handleDismiss = React.useCallback(() => {
clear();
props.onDismiss();
};
}, [clear, props.onDismiss]);

const handleKeyUp = (event: React.KeyboardEvent): void => {
if (event.keyCode === EnterKeyCode) {
const handleKeyUp = React.useCallback((event: React.KeyboardEvent): void => {
if (event.code === 'Enter') {
handleSave();
}
};
}, [handleSave]);

return (
<Form>
{ props.children }
<FormGroup isRequired label="Username" fieldId="username">
<TextInput
value={username}
Expand All @@ -90,7 +84,7 @@ export const JmxAuthForm: React.FunctionComponent<JmxAuthFormProps> = (props) =>
id="username"
onChange={setUsername}
onKeyUp={handleKeyUp}
autoFocus
autoFocus={props.focus}
/>
</FormGroup>
<FormGroup isRequired label="Password" fieldId="password">
Expand Down
3 changes: 2 additions & 1 deletion src/app/CreateRecording/CreateRecording.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ import { SnapshotRecordingForm } from './SnapshotRecordingForm';
export interface CreateRecordingProps {
recordingName?: string;
template?: string;
templateType?: string;
eventSpecifiers?: string[];
}

Expand Down Expand Up @@ -80,7 +81,7 @@ const Comp: React.FunctionComponent< RouteComponentProps<{}, StaticContext, Crea
})
);
};

const handleCreateSnapshot = (): void => {
addSubscription(
context.api.createSnapshot()
Expand Down
4 changes: 2 additions & 2 deletions src/app/Rules/CreateRule.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ import { BreadcrumbPage, BreadcrumbTrail } from '@app/BreadcrumbPage/BreadcrumbP
import { useSubscriptions } from '@app/utils/useSubscriptions';
import { EventTemplate } from '../CreateRecording/CreateRecording';
import { Rule } from './Rules';
import { MatchExpressionEvaluator } from './MatchExpressionEvaluator';
import { MatchExpressionEvaluator } from '../Shared/MatchExpressionEvaluator';
import { FormSelectTemplateSelector } from '../TemplateSelector/FormSelectTemplateSelector';

// FIXME check if this is correct/matches backend name validation
Expand Down Expand Up @@ -415,7 +415,7 @@ const Comp = () => {
</CardHeaderMain>
</CardHeader>
<CardBody>
<MatchExpressionEvaluator matchExpression={matchExpression} onChange={setMatchExpressionValid} />
<MatchExpressionEvaluator inlineHint matchExpression={matchExpression} onChange={setMatchExpressionValid} />
</CardBody>
</Card>
</GridItem>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,12 +36,12 @@
* SOFTWARE.
*/
import * as React from 'react';
import { Form, FormGroup, Modal, ModalVariant, ValidatedOptions } from '@patternfly/react-core';
import { FormGroup, Modal, ModalVariant, Text, TextInput, TextVariants, ValidatedOptions } from '@patternfly/react-core';
import { JmxAuthForm } from '@app/AppLayout/JmxAuthForm';
import { TargetSelect } from '@app/TargetSelect/TargetSelect';
import { NO_TARGET } from '@app/Shared/Services/Target.service';
import { ServiceContext } from '@app/Shared/Services/Services';
import { first } from 'rxjs';
import { MatchExpressionEvaluator } from '@app/Shared/MatchExpressionEvaluator';
import { useSubscriptions } from '@app/utils/useSubscriptions';

export interface CreateJmxCredentialModalProps {
visible: boolean;
Expand All @@ -50,22 +50,22 @@ export interface CreateJmxCredentialModalProps {

export const CreateJmxCredentialModal: React.FunctionComponent<CreateJmxCredentialModalProps> = (props) => {
const context = React.useContext(ServiceContext);
const [validTarget, setValidTarget] = React.useState(ValidatedOptions.default);
const addSubscription = useSubscriptions();
const [matchExpression, setMatchExpression] = React.useState('');
const [matchExpressionValid, setMatchExpressionValid] = React.useState(ValidatedOptions.default);

const onSave = React.useCallback((username: string, password: string) => {
let isValid;
context.target.target().subscribe((t) => {
isValid = t == NO_TARGET ? ValidatedOptions.error : ValidatedOptions.success;
setValidTarget(isValid);
const onSave = React.useCallback((username: string, password: string): Promise<void> => {
return new Promise((resolve) => {
addSubscription(
context.api.postCredentials(matchExpression, username, password)
.pipe(first())
.subscribe(() => {
props.onClose();
resolve();
})
);
});

if (isValid == ValidatedOptions.success) {
context.api.postTargetCredentials(username, password)
.pipe(first())
.subscribe();
props.onClose();
}
}, [props.onClose, context, context.target, context.api, validTarget, setValidTarget]);
}, [props.onClose, context, context.target, context.api, matchExpression]);

return (
<Modal
Expand All @@ -74,23 +74,36 @@ export const CreateJmxCredentialModal: React.FunctionComponent<CreateJmxCredenti
showClose={true}
onClose={props.onClose}
title="Store JMX Credentials"
description="Creates stored credentials for a given target.
If a Target JVM requires JMX authentication, Cryostat will use stored credentials
description="Creates stored credentials for target JVMs according to various properties.
If a Target JVM requires JMX authentication, Cryostat will use stored credentials
when attempting to open JMX connections to the target."
>
<Form>
<JmxAuthForm onSave={onSave} onDismiss={props.onClose} focus={false}>
<MatchExpressionEvaluator matchExpression={matchExpression} onChange={setMatchExpressionValid} />
<FormGroup
label="Target"
label="Match Expression"
isRequired
fieldId="target-select"
helperTextInvalid="Select a target"
validated={validTarget}
fieldId="match-expression"
helperText={
tthvo marked this conversation as resolved.
Show resolved Hide resolved
<Text component={TextVariants.small}>
Enter a match expression. This is a Java-like code snippet that is evaluated against each target application to determine whether the rule should be applied.
Select a target from the dropdown below to view the context object available within the match expression context and test if the expression matches.
</Text>
}
validated={matchExpressionValid}
>
<TargetSelect />
<TextInput
value={matchExpression}
isRequired
type="text"
id="rule-matchexpr"
aria-describedby="rule-matchexpr-helper"
onChange={setMatchExpression}
validated={matchExpressionValid}
autoFocus
/>
</FormGroup>
</Form>
<br/>
<JmxAuthForm onSave={onSave} onDismiss={props.onClose} />
</JmxAuthForm>
</Modal>
);
};
75 changes: 75 additions & 0 deletions src/app/SecurityPanel/Credentials/CredentialsTableRow.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
/*
* Copyright The Cryostat Authors
*
* The Universal Permissive License (UPL), Version 1.0
*
* Subject to the condition set forth below, permission is hereby granted to any
* person obtaining a copy of this software, associated documentation and/or data
* (collectively the "Software"), free of charge and under any and all copyright
* rights in the Software, and any and all patent rights owned or freely
* licensable by each licensor hereunder covering either (i) the unmodified
* Software as contributed to or provided by such licensor, or (ii) the Larger
* Works (as defined below), to deal in both
*
* (a) the Software, and
* (b) any piece of software and/or hardware listed in the lrgrwrks.txt file if
* one is included with the Software (each a "Larger Work" to which the Software
* is contributed by such licensors),
*
* without restriction, including without limitation the rights to copy, create
* derivative works of, display, perform, and distribute the Software and make,
* use, sell, offer for sale, import, export, have made, and have sold the
* Software and the Larger Work(s), and to sublicense the foregoing rights on
* either these or other terms.
*
* This license is subject to the following condition:
* The above copyright notice and either this complete permission notice or at
* a minimum a reference to the UPL must be included in all copies or
* substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
import * as React from 'react';
import { Checkbox } from '@patternfly/react-core';
import { Tbody, Td, Tr } from '@patternfly/react-table';

export interface CredentialsTableRowProps {
key: number;
index: number;
matchExpression: string;
isChecked: boolean;
label: string;
handleCheck: (state: boolean, index: number) => void;
}

export const CredentialsTableRow: React.FunctionComponent<CredentialsTableRowProps> = (props: CredentialsTableRowProps) => {

const handleCheck = React.useCallback((checked: boolean) => {
props.handleCheck(checked, props.index);
}, [props, props.handleCheck]);

return (
<Tbody key={props.index}>
<Tr key={`${props.index}`}>
<Td key={`credentials-table-row-${props.index}_0`}>
<Checkbox
name={`credentials-table-row-${props.index}-check`}
onChange={handleCheck}
isChecked={props.isChecked}
id={`credentials-table-row-${props.index}-check`}
aria-label={`credentials-table-row-${props.index}-check`}
/>
</Td>
<Td key={`credentials-table-row-${props.index}_1`} dataLabel={props.label}>
{props.matchExpression}
</Td>
</Tr>
</Tbody>
);
};
Loading