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

Improve/project admin keys #3882

Merged
merged 2 commits into from
Aug 2, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
import org.bouncycastle.openssl.jcajce.JcaPEMWriter;
import org.obiba.opal.core.security.OpalKeyStore;
import org.obiba.opal.core.service.security.KeyStoreService;
import org.obiba.opal.web.BaseResource;
import org.obiba.opal.web.magma.ClientErrorDtos;
import org.obiba.opal.web.model.Opal;
import org.springframework.beans.factory.annotation.Autowired;
Expand All @@ -52,7 +53,7 @@

@Component
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public class KeyStoreResource {
public class KeyStoreResource implements BaseResource {

private OpalKeyStore keyStore;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
:hint="$t('identity_provider.name_hint')"
class="q-mb-md"
lazy-rules
:rules="[validateRequiredField('validation.identity_provider.name_required')]"
:rules="[validateRequiredField('validation.name_required')]"
:disable="editMode"
>
</q-input>
Expand Down
2 changes: 1 addition & 1 deletion opal-ui/src/components/admin/users/AddUserDialog.vue
Original file line number Diff line number Diff line change
Expand Up @@ -168,7 +168,7 @@ function addGroup(val: string, done: any) {
}

// Validation rules
const validateRequiredName = (val: string) => (val && val.trim().length > 0) || t('validation.user.name_required');
const validateRequiredName = (val: string) => (val && val.trim().length > 0) || t('validation.name_required');
const validateRequiredCertificate = (val: string) =>
(editMode.value && (!val || val.length === 0)) ||
(val && val.trim().length > 0) ||
Expand Down
2 changes: 1 addition & 1 deletion opal-ui/src/components/profile/AddTokenDialog.vue
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,7 @@ watch(
);

// Validations
const validateRequiredName = (val: string) => (val && val.trim().length > 0) || t('validation.token.name_required');
const validateRequiredName = (val: string) => (val && val.trim().length > 0) || t('validation.name_required');

// Group options
const taskGroupOptions: { label: string; value: string }[] = [];
Expand Down
113 changes: 113 additions & 0 deletions opal-ui/src/components/project/AddKeyPairDialog.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
<template>
<q-dialog v-model="showDialog" @hide="onHide" persistent>
<q-card class="dialog-sm">
<q-card-section>
<div class="text-h6">{{ $t('project_admin.import_key') }}</div>
</q-card-section>

<q-separator />

<q-card-section style="max-height: 75vh" class="scroll">
<q-form ref="formRef" class="q-gutter-sm" persistent>
<q-input
v-model="keyPair.alias"
dense
type="text"
:label="$t('name')"
lazy-rules
:rules="[validateRequiredField('validation.name_required')]"
>
</q-input>
<q-input
v-model="keyPair.privateImport"
dense
type="textarea"
rows="10"
:label="$t('project_admin.private_key')"
lazy-rules
:rules="[validateRequiredField('validation.project_admin.private_key_required')]"
>
</q-input>
<q-input
v-model="keyPair.publicImport"
dense
type="textarea"
rows="10"
:label="$t('project_admin.public_key')"
lazy-rules
:rules="[validateRequiredField('validation.project_admin.public_key_required')]"
>
</q-input>
<div class="text-help">{{ $t('project_admin.import_key_info') }}</div>
</q-form>
</q-card-section>
<q-separator />

<q-card-actions align="right" class="bg-grey-3"
><q-btn flat :label="$t('cancel')" color="secondary" v-close-popup />
<q-btn flat :label="$t('add')" type="submit" color="primary" @click="onAdd" />
</q-card-actions>
</q-card>
</q-dialog>
</template>

<script lang="ts">
export default defineComponent({
name: 'AddKeyPairDialog',
});
</script>

<script setup lang="ts">
import { ProjectDto } from 'src/models/Projects';
import { notifyError } from 'src/utils/notify';
import { KeyForm, KeyType } from 'src/models/Opal';

interface DialogProps {
modelValue: boolean;
project: ProjectDto;
}

const projectsStore = useProjectsStore();
const emptyKeyForm = {
alias: '',
publicImport: '',
privateImport: '',
keyType: KeyType.KEY_PAIR,
} as KeyForm;

const { t } = useI18n();
const props = defineProps<DialogProps>();
const showDialog = ref(props.modelValue);
const formRef = ref();
const emit = defineEmits(['update:modelValue', 'update']);
const keyPair = ref({ ...emptyKeyForm } as KeyForm);
const validateRequiredField = (id: string) => (val: string) => (val && val.trim().length > 0) || t(id);

watch(
() => props.modelValue,
(value) => {
if (value) {
showDialog.value = value;
}
}
);

function onHide() {
showDialog.value = false;
keyPair.value = { ...emptyKeyForm };
emit('update:modelValue', false);
}

async function onAdd() {
const valid = await formRef.value.validate();
if (valid) {
try {
await projectsStore.addKeyPair(props.project.name, keyPair.value);
emit('update');
onHide();
} catch (error) {
notifyError(error);
}
}
}
</script>
3 changes: 2 additions & 1 deletion opal-ui/src/components/project/IdMappingsList.vue
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,8 @@ const columns = computed(() => [
label: t('project_admin.entity_type'),
align: 'left',
field: 'entityType',
style: 'width: 30%',
headerStyle: 'width: 30%; white-space: normal;',
style: 'width: 30%; white-space: normal;',
},
{
name: 'mapping',
Expand Down
131 changes: 131 additions & 0 deletions opal-ui/src/components/project/KeyPairsList.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
<template>
<slot name="title"></slot>

<!-- TODO: instead of disabling Add button, put a message and a link to admin/id mappings page if has permission -->

<q-table
flat
:rows="keyPairs"
:columns="columns"
row-key="name"
:pagination="initialPagination"
:hide-pagination="keyPairs.length <= initialPagination.rowsPerPage"
:loading="loading"
>
<template v-slot:top-left>
<q-btn
color="primary"
:label="$t('add')"
icon="add"
size="sm"
@click.prevent="onAdd"
/>
</template>
<template v-slot:body-cell-name="props">
<q-td :props="props" @mouseover="onOverRow(props.row)" @mouseleave="onLeaveRow(props.row)">
{{ props.value }}
<div class="float-right">
<q-btn
rounded
dense
flat
size="sm"
color="secondary"
:title="$t('delete')"
:icon="toolsVisible[props.row.alias] ? 'delete' : 'none'"
class="q-ml-xs"
@click="onDelete(props.row)"
/>
</div>
</q-td>
</template>
<template v-slot:body-cell-mapping="props">
<q-td :props="props" @mouseover="onOverRow(props.row)" @mouseleave="onLeaveRow(props.row)">
{{ props.value }}
</q-td>
</template>
</q-table>

<add-key-pair-dialog v-model="showAddDialog" :project="project" @update="$emit('update')" />
</template>

<script lang="ts">
export default defineComponent({
name: 'KeyPairsList',
});
</script>

<script setup lang="ts">
import { t } from 'src/boot/i18n';
import { ProjectDto } from 'src/models/Projects';
import { KeyForm, KeyType } from 'src/models/Opal';
import { notifyError } from 'src/utils/notify';
import AddKeyPairDialog from 'src/components/project/AddKeyPairDialog.vue';

interface Props {
project: ProjectDto;
}

const emit = defineEmits(['update']);
const props = defineProps<Props>();
const projectsStore = useProjectsStore();
const showAddDialog = ref(false);
const keyPairs = ref([]);
const loading = ref(false);
const toolsVisible = ref<{ [key: string]: boolean }>({});
const initialPagination = ref({
sortBy: 'type',
descending: false,
page: 1,
rowsPerPage: 10,
minRowsForPagination: 10,
});

const columns = computed(() => [
{
name: 'name',
required: true,
label: t('name'),
align: 'left',
field: 'alias',
headerStyle: 'width: 30%; white-space: normal;',
style: 'width: 30%; white-space: normal;',
},
{
name: 'type',
label: t('type'),
align: 'left',
field: 'keyType',
format: (val: KeyType) => t(`key_type.${val}`),
},
]);

// Handlers

function onOverRow(row: KeyForm) {
toolsVisible.value[row.alias] = true;
}

function onLeaveRow(row: KeyForm) {
toolsVisible.value[row.alias] = false;
}

function onAdd() {
showAddDialog.value = true;
}

async function onDelete(row: KeyForm) {
try {
await projectsStore.deleteKeyPair(props.project.name, row.alias);
emit('update');
} catch (error) {
notifyError(error);
}
}

onMounted(() => {
projectsStore.getKeyPairs(props.project.name).then((response) => {
keyPairs.value = response;
});
});
</script>
19 changes: 14 additions & 5 deletions opal-ui/src/i18n/en/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -304,6 +304,11 @@ export default {
title: 'Commit Details',
},
},
key_type: {
KEY_PAIR: 'Key Pair',
CERTIFICATE: 'Certificate',
UNRECOGNIZED: 'Unrecognized',
},
project_admin: {
properties: 'Project properties',
db_hint: 'Project tables (dictionaries and data) are stored in the database:',
Expand Down Expand Up @@ -334,6 +339,12 @@ export default {
id_mappings_info: 'Identifiers mappings listed below that match the entity type of the data are automatically selected during an import/export process.',
id_mappings_hint: 'The name of the mapping.',
id_mapping: 'Identifiers Mapping',
encryption_keys: 'Encryption Keys',
encryption_keys_info: 'Encrypted data will be automatically decrypted at importation time using the key pairs registered within the project.',
import_key: 'Import Key Pair',
import_key_info: 'Paste encryption key in PEM format.',
private_key: 'Private Key',
public_key: 'Public Key (Certificate)',
},
user_profile: {
title: 'My profile',
Expand Down Expand Up @@ -389,15 +400,14 @@ export default {
deleteProject: 'Delete',
},
validation: {
name_required: 'Name is required',
user: {
name_required: 'Name is required',
password_required: 'Password is required and must be at least 8 characters long',
certificate_required: 'Certificate is required',
confirm_password_required: 'Confirm password is required',
passwords_not_matching: 'Passwords do not match',
},
identity_provider: {
name_required: 'Name is required',
clientId_required: 'Client ID is required',
secret_required: 'Secret is required',
discovery_uri_required: 'Discovery URI is required',
Expand All @@ -410,16 +420,15 @@ export default {
old_password: 'Old password is required',
new_password: 'New password is required and must be at least 8 characters long',
},
token: {
name_required: 'Token name is required',
},
text_required: 'A text is required',
github: {
org_required: 'Github organization or username is required',
repo_required: 'Github repository name is required',
},
project_admin: {
backup_folder_required: 'Backup folder is required',
private_key_required: 'Private Key in PEM format is required',
public_key_required: 'Public Key (Certificate) in PEM format is required',
},
},
main: {
Expand Down
Loading