blob: d98e75d9622ebb083eb0461f65dd630fe3099708 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.netbeans.modules.cloud.oracle.actions;
import com.oracle.bmc.identity.Identity;
import com.oracle.bmc.identity.IdentityClient;
import com.oracle.bmc.identity.model.Compartment;
import com.oracle.bmc.identity.requests.ListCompartmentsRequest;
import com.oracle.bmc.identity.responses.ListCompartmentsResponse;
import com.oracle.bmc.identity.model.Tenancy;
import org.netbeans.api.db.explorer.DatabaseConnection;
import com.oracle.bmc.model.BmcException;
import com.oracle.bmc.vault.VaultsClient;
import com.oracle.bmc.vault.model.Base64SecretContentDetails;
import com.oracle.bmc.vault.model.CreateSecretDetails;
import com.oracle.bmc.vault.model.SecretContentDetails;
import com.oracle.bmc.vault.model.SecretReuseRule;
import com.oracle.bmc.vault.model.UpdateSecretDetails;
import com.oracle.bmc.vault.requests.CreateSecretRequest;
import com.oracle.bmc.vault.requests.ListSecretsRequest;
import com.oracle.bmc.vault.requests.UpdateSecretRequest;
import com.oracle.bmc.vault.responses.CreateSecretResponse;
import com.oracle.bmc.vault.responses.ListSecretsResponse;
import com.oracle.bmc.vault.responses.UpdateSecretResponse;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Base64;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.TreeMap;
import java.util.UUID;
import java.util.concurrent.atomic.AtomicReference;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import org.netbeans.api.progress.ProgressHandle;
import org.netbeans.modules.cloud.oracle.OCIManager;
import static org.netbeans.modules.cloud.oracle.OCIManager.getDefault;
import org.netbeans.modules.cloud.oracle.OCIProfile;
import org.netbeans.modules.cloud.oracle.OCISessionInitiator;
import org.netbeans.modules.cloud.oracle.compartment.CompartmentItem;
import org.netbeans.modules.cloud.oracle.items.OCID;
import org.netbeans.modules.cloud.oracle.items.OCIItem;
import org.netbeans.modules.cloud.oracle.items.TenancyItem;
import org.netbeans.modules.cloud.oracle.vault.KeyItem;
import org.netbeans.modules.cloud.oracle.vault.KeyNode;
import org.netbeans.modules.cloud.oracle.vault.SecretItem;
import org.netbeans.modules.cloud.oracle.vault.SecretNode;
import org.netbeans.modules.cloud.oracle.vault.VaultItem;
import org.netbeans.modules.cloud.oracle.vault.VaultNode;
import org.openide.DialogDescriptor;
import org.openide.DialogDisplayer;
import org.openide.NotifyDescriptor;
import org.openide.NotifyDescriptor.QuickPick.Item;
import org.openide.awt.ActionID;
import org.openide.awt.ActionReference;
import org.openide.awt.ActionReferences;
import org.openide.awt.ActionRegistration;
import org.openide.util.NbBundle;
import org.openide.util.Pair;
/**
*
* @author Jan Horvath
*/
@ActionID(
category = "Tools",
id = "org.netbeans.modules.cloud.oracle.actions.AddDbConnectionToVault"
)
@ActionRegistration(
displayName = "#AddADBToVault",
asynchronous = true
)
@ActionReferences(value = {
@ActionReference(path = "Cloud/Oracle/Databases/Actions", position = 250)
})
@NbBundle.Messages({
"AddADBToVault=Add Oracle Autonomous DB details to OCI Vault",
"SelectKey=Select Key",
"SelectVault=Select Vault",
"SecretsCreated=Secrets were created or updated",
"NoKeys=No keys in this Vault. Select another one.",
"DatasourceName=Datasource Name",
"AddVersion=Add new versions",
"Cancel=Cancel",
"SecretExists=Secrets with name {0} already exists",
"NoProfile=There is not any OCI profile in the config",
"NoCompartment=There are no compartments in the Tenancy"
})
public class AddDbConnectionToVault implements ActionListener {
private static final Logger LOG = Logger.getLogger(AddDbConnectionToVault.class.getName());
private final DatabaseConnection context;
public AddDbConnectionToVault(DatabaseConnection context) {
this.context = context;
}
static interface Step<T, U> {
Step<T, U> prepare(T item);
NotifyDescriptor createInput();
boolean onlyOneChoice();
Step getNext();
void setValue(String selected);
U getValue();
}
class TenancyStep implements Step<Object, TenancyItem> {
List<OCIProfile> profiles = new LinkedList<>();
private AtomicReference<TenancyItem> selected = new AtomicReference<>();
@Override
public NotifyDescriptor createInput() {
if (onlyOneChoice()) {
throw new IllegalStateException("No data to create input"); // NOI18N
}
String title = Bundle.SelectProfile();
List<NotifyDescriptor.QuickPick.Item> items = new ArrayList<>(profiles.size());
for (OCIProfile p : profiles) {
Tenancy t = p.getTenancyData();
if (t != null) {
items.add(new NotifyDescriptor.QuickPick.Item(p.getId(), Bundle.SelectProfile_Description(t.getName(), t.getHomeRegionKey())));
}
}
if (profiles.stream().filter(p -> p.getTenancy().isPresent()).count() == 0) {
title = Bundle.NoProfile();
}
return new NotifyDescriptor.QuickPick(title, title, items, false);
}
@Override
public Step getNext() {
return new CompartmentStep().prepare(getValue());
}
public Step<Object, TenancyItem> prepare(Object i) {
ProgressHandle h = ProgressHandle.createHandle(Bundle.MSG_CollectingProfiles());
h.start();
h.progress(Bundle.MSG_CollectingProfiles_Text());
try {
profiles = OCIManager.getDefault().getConnectedProfiles();
} finally {
h.finish();
}
return this;
}
public void setValue(String selected) {
for (OCIProfile profile : profiles) {
if (profile.getId().equals(selected)) {
profile.getTenancy().ifPresent(t -> this.selected.set(t));
break;
}
}
}
@Override
public TenancyItem getValue() {
if (onlyOneChoice()) {
return profiles.stream()
.map(p -> p.getTenancy())
.filter(Optional::isPresent)
.map(Optional::get)
.findFirst()
.get();
}
return selected.get();
}
@Override
public boolean onlyOneChoice() {
return profiles.stream().filter(p -> p.getTenancy().isPresent()).count() == 1;
}
}
static class CompartmentStep implements Step<TenancyItem, CompartmentItem> {
private Map<String, OCIItem> compartments = null;
private CompartmentItem selected;
public Step<TenancyItem, CompartmentItem> prepare(TenancyItem tenancy) {
ProgressHandle h = ProgressHandle.createHandle(Bundle.MSG_CollectingItems());
h.start();
h.progress(Bundle.MSG_CollectingItems_Text());
try {
compartments = getFlatCompartment(tenancy);
} finally {
h.finish();
}
return this;
}
@Override
public NotifyDescriptor createInput() {
if (onlyOneChoice()) {
throw new IllegalStateException("Input shouldn't be displayed for one choice"); // NOI18N
}
if (compartments.isEmpty()) {
createQuickPick(compartments, Bundle.NoCompartment());
}
return createQuickPick(compartments, Bundle.SelectCompartment());
}
@Override
public Step getNext() {
return new VaultStep().prepare(getValue());
}
@Override
public void setValue(String selected) {
this.selected = (CompartmentItem) compartments.get(selected);
}
@Override
public CompartmentItem getValue() {
if (onlyOneChoice()) {
return (CompartmentItem) compartments.values().iterator().next();
}
return selected;
}
@Override
public boolean onlyOneChoice() {
return compartments.size() == 1;
}
}
static class VaultStep implements Step<CompartmentItem, VaultItem> {
private Map<String, VaultItem> vaults = null;
private VaultItem selected;
public Step<CompartmentItem, VaultItem> prepare(CompartmentItem compartment) {
ProgressHandle h = ProgressHandle.createHandle(Bundle.MSG_CollectingItems());
h.start();
h.progress(Bundle.MSG_CollectingItems_Text());
try {
vaults = getVaults(compartment);
} finally {
h.finish();
}
return this;
}
@Override
public NotifyDescriptor createInput() {
return createQuickPick(vaults, Bundle.SelectVault());
}
@Override
public Step getNext() {
return new KeyStep().prepare(selected);
}
@Override
public void setValue(String selected) {
this.selected = vaults.get(selected);
}
@Override
public VaultItem getValue() {
if (onlyOneChoice()) {
vaults.values().iterator().next();
}
return selected;
}
@Override
public boolean onlyOneChoice() {
return vaults.size() == 1;
}
}
static class KeyStep implements Step<VaultItem, Pair<VaultItem, KeyItem>> {
private Map<String, KeyItem> keys = null;
private KeyItem selected;
private VaultItem vault;
public Step<VaultItem, Pair<VaultItem, KeyItem>> prepare(VaultItem vault) {
this.vault = vault;
ProgressHandle h = ProgressHandle.createHandle(Bundle.MSG_CollectingItems());
h.start();
h.progress(Bundle.MSG_CollectingItems_Text());
try {
keys = getKeys(vault);
} finally {
h.finish();
}
return this;
}
@Override
public boolean onlyOneChoice() {
return keys.size() == 1;
}
@Override
public NotifyDescriptor createInput() {
if (keys.size() > 1) {
return createQuickPick(keys, Bundle.SelectKey());
}
if (keys.size() == 0) {
return new NotifyDescriptor.QuickPick("", Bundle.NoKeys(), Collections.emptyList(), false);
}
throw new IllegalStateException("No data to create input"); // NOI18N
}
@Override
public Step getNext() {
return new DatasourceNameStep().prepare(getValue());
}
@Override
public void setValue(String selected) {
this.selected = keys.get(selected);
}
@Override
public Pair<VaultItem, KeyItem> getValue() {
if (keys.size() == 1) {
return Pair.of(vault, keys.values().iterator().next());
}
return Pair.of(vault, selected);
}
}
static class DatasourceNameStep implements Step<Pair<VaultItem, KeyItem>, Result> {
private Result result = new Result();
@Override
public Step<Pair<VaultItem, KeyItem>, Result> prepare(Pair<VaultItem, KeyItem> item) {
result.vault = item.first();
result.key = item.second();
return this;
}
@Override
public NotifyDescriptor createInput() {
return new NotifyDescriptor.InputLine("DEFAULT", Bundle.DatasourceName()); //NOI18N
}
@Override
public Step getNext() {
return new OverwriteStep().prepare(result);
}
@Override
public void setValue(String selected) {
result.datasourceName = selected;
}
@Override
public Result getValue() {
return result;
}
@Override
public boolean onlyOneChoice() {
return false;
}
}
static class OverwriteStep implements Step<Result, Result> {
private Result result;
private Set<String> dsNames;
private String choice;
@Override
public Step<Result, Result> prepare(Result result) {
this.result = result;
List<SecretItem> secrets = SecretNode.getSecrets().apply(result.vault);
this.dsNames = secrets.stream()
.map(s -> extractDatasourceName(s.getName()))
.filter(Objects::nonNull)
.collect(Collectors.toSet());
return this;
}
@Override
public NotifyDescriptor createInput() {
List<Item> yesNo = new ArrayList();
yesNo.add(new Item(Bundle.AddVersion(), ""));
yesNo.add(new Item(Bundle.Cancel(), ""));
return new NotifyDescriptor.QuickPick("", Bundle.SecretExists(result.datasourceName), yesNo, false);
}
@Override
public Step getNext() {
return null;
}
@Override
public void setValue(String choice) {
this.choice = choice;
}
@Override
public Result getValue() {
if (Bundle.AddVersion().equals(choice) || onlyOneChoice()) {
result.update = true;
return result;
}
return null;
}
@Override
public boolean onlyOneChoice() {
return !dsNames.contains(result.datasourceName);
}
}
static class Result {
VaultItem vault;
KeyItem key;
String datasourceName;
private boolean update;
}
static class Multistep {
private final LinkedList<Step> steps = new LinkedList<>();
Multistep(Step firstStep) {
steps.add(firstStep);
}
NotifyDescriptor.ComposedInput.Callback createInput() {
return new NotifyDescriptor.ComposedInput.Callback() {
private void showInput(Step step, NotifyDescriptor desc) {
String selected = null;
if (!step.onlyOneChoice()) {
if (desc instanceof NotifyDescriptor.QuickPick) {
for (NotifyDescriptor.QuickPick.Item item : ((NotifyDescriptor.QuickPick) desc).getItems()) {
if (item.isSelected()) {
selected = item.getLabel();
break;
}
}
} else if (desc instanceof NotifyDescriptor.InputLine) {
selected = ((NotifyDescriptor.InputLine) desc).getInputText();
}
step.setValue(selected);
}
}
NotifyDescriptor prepareInput(NotifyDescriptor.ComposedInput input, int number) {
if (number == 1) {
steps.get(0).prepare(null);
return steps.get(0).createInput();
}
if (steps.size() > number) {
steps.removeLast();
return steps.getLast().createInput();
}
showInput(steps.getLast(), input.getInputs()[number - 2]);
Step currentStep = steps.getLast().getNext();
if (currentStep == null) {
return null;
}
steps.add(currentStep);
if (currentStep.onlyOneChoice()) {
return prepareInput(input, number);
}
return currentStep.createInput();
}
@Override
public NotifyDescriptor createInput(NotifyDescriptor.ComposedInput input, int number) {
return prepareInput(input, number);
}
};
}
Object getResult() {
return steps.getLast().getValue();
}
}
@Override
public void actionPerformed(ActionEvent e) {
Multistep multistep = new Multistep(new TenancyStep());
NotifyDescriptor.ComposedInput ci = new NotifyDescriptor.ComposedInput(Bundle.AddADB(), 3, multistep.createInput());
if (DialogDescriptor.OK_OPTION == DialogDisplayer.getDefault().notify(ci)) {
if (multistep.getResult() != null) {
Result v = (Result) multistep.getResult();
addDbConnectionToVault(v.vault, v.key, context, v.datasourceName);
}
}
}
private static void addDbConnectionToVault(VaultItem vault, KeyItem key, DatabaseConnection connection, String datasourceName) {
VaultsClient client = VaultsClient.builder().build(getDefault().getActiveProfile().getConfigProvider());
ListSecretsRequest listSecretsRequest = ListSecretsRequest.builder()
.compartmentId(vault.getCompartmentId())
.vaultId(vault.getKey().getValue())
.limit(88)
.build();
ListSecretsResponse secrets = client.listSecrets(listSecretsRequest);
Map<String, String> existingSecrets = secrets.getItems().stream()
.collect(Collectors.toMap(s -> s.getSecretName(), s -> s.getId()));
Map<String, String> values = new HashMap<String, String>() {
{
put("Username", connection.getUser()); //NOI18N
put("Password", connection.getPassword()); //NOI18N
put("OCID", (String) connection.getConnectionProperties().get("OCID")); //NOI18N
put("wallet_Password", UUID.randomUUID().toString()); //NOI18N
}
};
try {
for (Entry<String, String> entry : values.entrySet()) {
String secretName = "DATASOURCES_" + datasourceName + "_" + entry.getKey().toUpperCase(); //NOI18N
String base64Content = Base64.getEncoder().encodeToString(entry.getValue().getBytes(StandardCharsets.UTF_8));
SecretContentDetails contentDetails = Base64SecretContentDetails.builder()
.content(base64Content)
.stage(SecretContentDetails.Stage.Current).build();
if (existingSecrets.containsKey(secretName)) {
UpdateSecretDetails updateSecretDetails = UpdateSecretDetails.builder()
.secretContent(contentDetails)
.build();
UpdateSecretRequest request = UpdateSecretRequest.builder()
.secretId(existingSecrets.get(secretName))
.updateSecretDetails(updateSecretDetails)
.build();
UpdateSecretResponse response = client.updateSecret(request);
} else {
CreateSecretDetails createDetails = CreateSecretDetails.builder()
.secretName(secretName)
.secretContent(contentDetails)
.secretRules(new ArrayList<>(Arrays.asList(SecretReuseRule.builder()
.isEnforcedOnDeletedSecretVersions(false).build())))
.compartmentId(vault.getCompartmentId())
.vaultId(vault.getKey().getValue())
.keyId(key.getKey().getValue())
.build();
CreateSecretRequest request = CreateSecretRequest
.builder()
.createSecretDetails(createDetails)
.build();
CreateSecretResponse response = client.createSecret(request);
}
}
} catch (BmcException e) {
NotifyDescriptor.Message msg = new NotifyDescriptor.Message(e.getMessage());
DialogDisplayer.getDefault().notify(msg);
throw new RuntimeException(e);
}
NotifyDescriptor.Message msg = new NotifyDescriptor.Message(Bundle.SecretsCreated());
DialogDisplayer.getDefault().notify(msg);
}
private static <T extends OCIItem> NotifyDescriptor.QuickPick createQuickPick(Map<String, T> ociItems, String title) {
List<NotifyDescriptor.QuickPick.Item> items = ociItems.entrySet().stream()
.map(entry -> new NotifyDescriptor.QuickPick.Item(entry.getKey(), entry.getValue().getDescription()))
.collect(Collectors.toList());
return new NotifyDescriptor.QuickPick(title, title, items, false);
}
private static Map<String, OCIItem> getFlatCompartment(TenancyItem tenancy) {
Map<OCID, FlatCompartmentItem> compartments = new HashMap<>();
OCISessionInitiator session = OCIManager.getDefault().getActiveSession();
Identity identityClient = session.newClient(IdentityClient.class);
String nextPageToken = null;
do {
ListCompartmentsResponse response
= identityClient.listCompartments(
ListCompartmentsRequest.builder()
.compartmentId(tenancy.getKey().getValue())
.compartmentIdInSubtree(true)
.lifecycleState(Compartment.LifecycleState.Active)
.accessLevel(ListCompartmentsRequest.AccessLevel.Accessible)
.limit(1000)
.page(nextPageToken)
.build());
for (Compartment comp : response.getItems()) {
FlatCompartmentItem ci = new FlatCompartmentItem(comp) {
FlatCompartmentItem getItem(OCID compId) {
return compartments.get(compId);
}
};
compartments.put(ci.getKey(), ci);
}
nextPageToken = response.getOpcNextPage();
} while (nextPageToken != null);
Map<String, OCIItem> pickItems = computeFlatNames(compartments);
pickItems.put(tenancy.getName() + " (root)", tenancy); // NOI18N
return pickItems;
}
private static Map<String, OCIItem> computeFlatNames(Map<OCID, FlatCompartmentItem> compartments) {
Map<String, OCIItem> pickItems = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
for (FlatCompartmentItem comp : compartments.values()) {
pickItems.put(comp.getName(), comp);
}
return pickItems;
}
private static abstract class FlatCompartmentItem extends CompartmentItem {
private final OCID parentId;
private String flatName;
private FlatCompartmentItem(Compartment ociComp) {
super(OCID.of(ociComp.getId(), "Compartment"), ociComp.getName()); // NOI18N
setDescription(ociComp.getDescription());
parentId = OCID.of(ociComp.getCompartmentId(), "Compartment"); // NOI18N
}
public String getName() {
if (parentId.getValue() == null) {
return "";
}
if (flatName == null) {
String parentFlatName = "";
FlatCompartmentItem parentComp = getItem(parentId);
if (parentComp != null) {
parentFlatName = parentComp.getName();
}
flatName = super.getName();
if (!parentFlatName.isEmpty()) {
flatName = parentFlatName + "/" + flatName; // NOI18N
}
}
return flatName;
}
abstract FlatCompartmentItem getItem(OCID compId);
}
protected static Map<String, VaultItem> getVaults(OCIItem parent) {
Map<String, VaultItem> items = new HashMap<>();
try {
if (parent instanceof CompartmentItem) {
VaultNode.getVaults().apply((CompartmentItem) parent).forEach((db) -> items.put(db.getName(), db));
}
} catch (BmcException e) {
LOG.log(Level.SEVERE, "Unable to load vault list", e); //NOI18N
}
return items;
}
protected static Map<String, KeyItem> getKeys(OCIItem parent) {
Map<String, KeyItem> items = new HashMap<>();
try {
if (parent instanceof VaultItem) {
KeyNode.getKeys().apply((VaultItem) parent).forEach((db) -> items.put(db.getName(), db));
}
} catch (BmcException e) {
LOG.log(Level.SEVERE, "Unable to load vault list", e); //NOI18N
}
return items;
}
static Pattern p = Pattern.compile("[A-Z]*_([A-Z]*)_[A-Z]*"); //NOI18N
protected static String extractDatasourceName(String value) {
Matcher m = p.matcher(value);
if (m.matches()) {
return m.group(1);
}
return null;
}
}