Skip to content

Commit

Permalink
feat: migrate all commands
Browse files Browse the repository at this point in the history
  • Loading branch information
35C4n0r committed Dec 30, 2024
1 parent 1fd70b4 commit 427e1cd
Show file tree
Hide file tree
Showing 15 changed files with 548 additions and 334 deletions.
2 changes: 1 addition & 1 deletion source/commands/gitops/create/github.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ type Props = {

export default function GitHub({ options }: Props) {
return (
<AuthProvider>
<AuthProvider permit_key={options.key}>
<GitHubComponent
authKey={options.key}
inactivateWhenValidated={options.inactive}
Expand Down
132 changes: 7 additions & 125 deletions source/commands/opa/policy.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,9 @@
import React from 'react';
import { Box, Newline, Text } from 'ink';
import zod from 'zod';
import { option } from 'pastel';
import Spinner from 'ink-spinner';
import { keyAccountOption } from '../../options/keychain.js';
import { inspect } from 'util';
import { loadAuthToken } from '../../lib/auth.js';
import { TextInput, Select } from '@inkjs/ui';
import Fuse from 'fuse.js';
import { AuthProvider } from '../../components/AuthProvider.js';
import OPAPolicyComponent from '../../components/opa/OPAPolicyComponent.js';

export const options = zod.object({
serverUrl: zod
Expand All @@ -31,130 +27,16 @@ export const options = zod.object({
keyAccount: keyAccountOption,
});

type Props = {
export type OpaPolicyProps = {
options: zod.infer<typeof options>;
};

interface PolicyItem {
id: string;
name?: string;
}

interface Option {
label: string;
value: string;
}

interface QueryResult {
result: { result: PolicyItem[] };
status: number;
}

export default function Policy({ options }: Props) {
const [error, setError] = React.useState<Error | null>(null);
const [res, setRes] = React.useState<QueryResult>({
result: { result: [] },
status: 0,
});
const [selection, setSelection] = React.useState<PolicyItem | undefined>(
undefined,
);
const [selectionFilter, setSelectionFilter] = React.useState<string>('');

const queryOPA = async (apiKey: string, path?: string) => {
const document = path ? `/${path}` : '';
const response = await fetch(
`${options.serverUrl}/v1/policies${document}`,
{ headers: apiKey ? { Authorization: `Bearer ${apiKey}` } : {} },
);
const data = await response.json();
setRes({ result: data, status: response.status });
};

React.useEffect(() => {
const performQuery = async () => {
const apiKey = options.apiKey || (await loadAuthToken());
await queryOPA(apiKey);
};
performQuery().catch(err => setError(err));
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [options.apiKey, options.serverUrl]);

const policyItems: Option[] = res.result.result.map(i => ({
label: i.id,
value: i.id,
}));

const fuse = new Fuse(policyItems, {
keys: ['label', 'id'],
minMatchCharLength: 0,
});
const filtered = fuse.search(selectionFilter).map(i => i.item);
const view = filtered.length === 0 ? policyItems : filtered;

const handleSelection = (selectedValue: string) => {
const selectedPolicy = res.result.result.find(p => p.id === selectedValue);
setSelection(selectedPolicy);
};

export default function Policy({ options }: OpaPolicyProps) {
return (
<>
<Text color={'green'}>
Listing Policies on Opa Server={options.serverUrl}
</Text>
{res.status === 0 && error === null && <Spinner type="dots" />}
{res.status === 200 && (
<>
{!selection && (
<>
<Text>
Showing {view.length} of {policyItems.length} policies:
</Text>

<Box flexDirection="column" gap={1}>
<TextInput
placeholder="Type text to filter list"
onSubmit={(value: string) => {
const selectedPolicy = res.result.result.find(
p => p.id === value,
);
setSelection(selectedPolicy);
}}
onChange={setSelectionFilter}
suggestions={policyItems.map(i => i.label)}
/>
</Box>
<Box padding={2} flexDirection="column" gap={1}>
<Select options={policyItems} onChange={handleSelection} />
</Box>
</>
)}
{!!selection && (
<Box flexDirection="column" gap={1}>
<Text>
{inspect(selection, {
colors: true,
depth: null,
maxArrayLength: Infinity,
})}
</Text>
</Box>
)}
</>
)}
{error && (
<Box>
<Text color="red">Request failed: {JSON.stringify(error)}</Text>
<Newline />
<Text>
{inspect(res, {
colors: true,
depth: null,
maxArrayLength: Infinity,
})}
</Text>
</Box>
)}
<AuthProvider>
<OPAPolicyComponent options={options} />
</AuthProvider>
</>
);
}
129 changes: 11 additions & 118 deletions source/commands/pdp/check.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,9 @@
import React from 'react';
import { Box, Newline, Text } from 'ink';
import zod, { string } from 'zod';
import { option } from 'pastel';
import { CLOUD_PDP_URL, KEYSTORE_PERMIT_SERVICE_NAME } from '../../config.js';
import Spinner from 'ink-spinner';
import { keyAccountOption } from '../../options/keychain.js';
import * as keytar from 'keytar';
import { inspect } from 'util';
import { parseAttributes } from '../../utils/attributes.js';
import PDPCheckComponent from '../../components/pdp/PDPCheckComponent.js';
import { AuthProvider } from '../../components/AuthProvider.js';

export const options = zod.object({
user: zod
Expand Down Expand Up @@ -89,123 +85,20 @@ export const options = zod.object({
keyAccount: keyAccountOption,
});

type Props = {
export type PDPCheckProps = {
options: zod.infer<typeof options>;
};

interface AllowedResult {
allow?: boolean;
}

export default function Check({ options }: Props) {
const [error, setError] = React.useState('');
const [res, setRes] = React.useState<AllowedResult>({ allow: undefined });

const queryPDP = async (apiKey: string) => {
try {
const userAttrs = options.userAttributes
? parseAttributes(options.userAttributes)
: {};
const resourceAttrs = options.resourceAttributes
? parseAttributes(options.resourceAttributes)
: {};

const response = await fetch(
`${options.pdpurl || CLOUD_PDP_URL}/allowed`,
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
...(apiKey ? { Authorization: `Bearer ${apiKey}` } : {}),
},
body: JSON.stringify({
user: {
key: options.user,
...userAttrs,
},
resource: {
type: options.resource.includes(':')
? options.resource.split(':')[0]
: options.resource,
key: options.resource.includes(':')
? options.resource.split(':')[1]
: '',
tenant: options.tenant,
...resourceAttrs,
},
action: options.action,
}),
},
);

if (!response.ok) {
const errorText = await response.text();
setError(errorText);
return;
}

setRes(await response.json());
} catch (err) {
if (err instanceof Error) {
setError(err.message);
} else {
setError(String(err));
}
}
};

React.useEffect(() => {
keytar
.getPassword(KEYSTORE_PERMIT_SERVICE_NAME, options.keyAccount)
.then(value => {
const apiKey = value || '';
queryPDP(apiKey);
})
.catch(reason => {
if (reason instanceof Error) {
setError(reason.message);
} else {
setError(String(reason));
}
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [options.keyAccount]);

export default function Check({ options }: PDPCheckProps) {
return (
<>
{/* The following text adheres to react/no-unescaped-entities rule */}
<Text>
Checking user=&quot;{options.user}&quot;
{options.userAttributes && ` with attributes=${options.userAttributes}`}
action={options.action} resource=
{options.resource}
{options.resourceAttributes &&
` with attributes=${options.resourceAttributes}`}
at tenant={options.tenant}
</Text>
{res.allow === true && (
<>
<Text color={'green'}> ALLOWED </Text>
<Box marginLeft={4}>
<Text>
{inspect(res, {
colors: true,
depth: null,
maxArrayLength: Infinity,
})}
</Text>
</Box>
</>
)}
{res.allow === false && <Text color={'red'}> DENIED</Text>}
{res.allow === undefined && error === '' && <Spinner type="dots" />}
{error && (
<Box>
<Text color="red">Request failed: {error}</Text>
<Newline />
<Text>{JSON.stringify(res)}</Text>
</Box>
)}
<AuthProvider
scope={'environment'}
permit_key={options.apiKey}
keyAccount={options.keyAccount}
>
<PDPCheckComponent options={options} />
</AuthProvider>
</>
);
}
6 changes: 3 additions & 3 deletions source/commands/pdp/run.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import React from 'react';
import { AuthProvider } from '../../components/AuthProvider.js';
import PDPCommand from '../../components/PDPCommand.js';
import PDPRunComponent from '../../components/pdp/PDPRunComponent.js';
import { type infer as zInfer, number, object } from 'zod';
import { option } from 'pastel';

Expand All @@ -16,8 +16,8 @@ type Props = {

export default function Run({ options: { opa } }: Props) {
return (
<AuthProvider>
<PDPCommand opa={opa} />
<AuthProvider scope={'environment'}>
<PDPRunComponent opa={opa} />
</AuthProvider>
);
}
25 changes: 14 additions & 11 deletions source/components/AuthProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,12 +30,14 @@ type AuthProviderProps = {
readonly children: ReactNode;
permit_key?: string | null;
scope?: 'organization' | 'project' | 'environment';
keyAccount?: string | null;
};

export function AuthProvider({
children,
permit_key: key,
scope,
keyAccount,
}: AuthProviderProps) {
const { validateApiKeyScope, getApiKeyList, getApiKeyById, getApiKeyScope } =
useApiKeyApi();
Expand Down Expand Up @@ -68,18 +70,20 @@ export function AuthProvider({
}, [authToken, currentScope]);

useEffect(() => {
const fetchAuthToken = async () => {
const fetchAuthToken = async (
redirect_scope: 'organization' | 'project' | 'login',
) => {
try {
const token = await loadAuthToken();
setAuthToken(token);
const token = await loadAuthToken(keyAccount);
const { response, error } = await getApiKeyScope(token);
if (error) {
setError(error);
return;
}
setAuthToken(token);
setCurrentScope(response);
} catch {
setState('login');
setState(redirect_scope);
}
};

Expand All @@ -88,21 +92,20 @@ export function AuthProvider({
setState('validate');
} else {
if (scope === 'environment') {
setState('login');
} else if (scope === 'project') {
setState('project');
} else if (scope === 'organization') {
setState('organization');
fetchAuthToken('login');
} else if (scope === 'project' || scope === 'organization') {
fetchAuthToken(scope);
}
}
} else {
if (key) {
setState('validate');
} else {
fetchAuthToken();
fetchAuthToken('login');
}
}
}, [getApiKeyScope, key, scope]);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [key, keyAccount, scope]);

useEffect(() => {
if (state === 'validate') {
Expand Down
Loading

0 comments on commit 427e1cd

Please sign in to comment.