Skip to content

Commit 851c68e

Browse files
committed
feat: add credentials command to store encrypted credentials in a dedicated config file (#60)
1 parent c8be54a commit 851c68e

File tree

2 files changed

+225
-0
lines changed

2 files changed

+225
-0
lines changed

eodag/cli.py

+202
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
4040
noqa: D103
4141
"""
42+
import getpass
4243
import json
4344
import os
4445
import shutil
@@ -48,9 +49,16 @@
4849
import click
4950

5051
from eodag.api.core import DEFAULT_ITEMS_PER_PAGE, DEFAULT_PAGE, EODataAccessGateway
52+
from eodag.config import (
53+
get_provider_credentials,
54+
save_providers_credentials,
55+
set_provider_credentials_key,
56+
)
5157
from eodag.utils import parse_qs
58+
from eodag.utils.cli import ask_confirmation
5259
from eodag.utils.exceptions import NoMatchingProductType, UnsupportedProvider
5360
from eodag.utils.logging import setup_logging
61+
from eodag.utils.security import Crypto
5462

5563
# A list of supported crunchers that the user can choose (see --cruncher option below)
5664
CRUNCHERS = [
@@ -830,5 +838,199 @@ def deploy_wsgi_app(
830838
click.echo(apache_config_sample)
831839

832840

841+
@eodag.command(
842+
name="credentials",
843+
help="This command can be used to create, read, update, or delete a provider's credentials. "
844+
"Default behavior is create/update.",
845+
)
846+
@click.option(
847+
"-f",
848+
"--conf",
849+
type=click.Path(exists=True),
850+
help="File path to the user configuration file, default is ~/.config/eodag/eodag.yml",
851+
)
852+
@click.option(
853+
"-c",
854+
"--creds",
855+
type=click.Path(exists=False),
856+
help="File path to the credentials file, default is ~/.config/eodag/credentials",
857+
)
858+
@click.option(
859+
"-y",
860+
"--yes",
861+
is_flag=True,
862+
help="Answers yes to command confirmation, disabling safety of credentials update, read and deletion",
863+
)
864+
@click.option(
865+
"-l",
866+
"--list",
867+
is_flag=True,
868+
help="Display the provider's credentials fields names",
869+
)
870+
@click.option(
871+
"-e",
872+
"--exists",
873+
is_flag=True,
874+
help="Check if the provider's credentials exists in the credentials file. "
875+
"WARNING: Raw credentials in the user conf are not checked",
876+
)
877+
@click.option(
878+
"-s",
879+
"--set",
880+
multiple=True,
881+
help="Set a provider's credentials value",
882+
)
883+
@click.option(
884+
"-r",
885+
"--read",
886+
is_flag=True,
887+
help="Read the provider's credentials and display them. WARNING: "
888+
"This command may display sensible informations, be wary of it",
889+
)
890+
@click.option(
891+
"-d",
892+
"--delete",
893+
is_flag=True,
894+
help="Delete the provider's credentials",
895+
)
896+
@click.argument("provider", type=str)
897+
@click.pass_context
898+
def credentials(ctx, **kwargs):
899+
"""Command used to create, read, update, or delete a provider's credentials, Default behavior is create/update."""
900+
setup_logging(verbose=ctx.obj["verbosity"])
901+
902+
conf_file = kwargs.pop("conf")
903+
if conf_file:
904+
conf_file = click.format_filename(conf_file)
905+
creds_file = kwargs.pop("creds")
906+
if creds_file:
907+
creds_file = click.format_filename(creds_file)
908+
909+
dag = EODataAccessGateway(
910+
user_conf_file_path=conf_file, credentials_file_path=creds_file
911+
)
912+
providers = dag.available_providers()
913+
provider = kwargs.pop("provider")
914+
915+
if provider not in providers:
916+
click.echo(
917+
f"Error: Provider '{provider}' not found in available providers.",
918+
err=True,
919+
)
920+
sys.exit(-1)
921+
922+
provider_credentials = get_provider_credentials(dag.providers_config[provider])
923+
encrypted_credentials = {**dag.encrypted_credentials}
924+
provider_has_encrypted_credentials = provider in dag.encrypted_credentials
925+
926+
do_list = kwargs.pop("list")
927+
if do_list:
928+
creds_names = list(provider_credentials.keys())
929+
click.echo(
930+
f"Credentials field{'s' if len(creds_names)>1 else ''} for '{provider}': ",
931+
nl=False,
932+
)
933+
click.echo(", ".join(creds_names))
934+
sys.exit(0)
935+
936+
do_exists = kwargs.pop("exists")
937+
if do_exists:
938+
if provider_has_encrypted_credentials:
939+
click.echo(f"Credentials found for provider '{provider}'.")
940+
sys.exit(0)
941+
else:
942+
click.echo(
943+
f"Error: No credentials found for the provider '{provider}' inside '{dag.credentials_file_path}'.",
944+
err=True,
945+
)
946+
sys.exit(-1)
947+
948+
confirm_yes = kwargs.pop("yes")
949+
950+
do_read = kwargs.pop("read")
951+
if do_read:
952+
if provider in dag.encrypted_credentials:
953+
if not confirm_yes:
954+
confirm = ask_confirmation(
955+
f"Do you want to read the credentials saved for '{provider}' "
956+
f"inside '{dag.credentials_file_path}'?"
957+
)
958+
if not confirm:
959+
sys.exit(1)
960+
for cred_name, cred in provider_credentials.items():
961+
click.echo(f"{cred_name}: {cred}")
962+
sys.exit(0)
963+
else:
964+
click.echo(
965+
f"Error: No credentials found for the provider '{provider}' "
966+
f"inside '{dag.credentials_file_path}', cannot read."
967+
"",
968+
err=True,
969+
)
970+
sys.exit(-1)
971+
972+
do_delete = kwargs.pop("delete")
973+
if do_delete:
974+
if provider in dag.encrypted_credentials:
975+
if not confirm_yes:
976+
confirm = ask_confirmation(
977+
f"Do you want to delete the credentials saved for '{provider}' "
978+
f"inside '{dag.credentials_file_path}'?"
979+
)
980+
if not confirm:
981+
sys.exit(1)
982+
del encrypted_credentials[provider]
983+
save_providers_credentials(dag.credentials_file_path, encrypted_credentials)
984+
click.echo(f"Credentials of '{provider}' were deleted.")
985+
sys.exit(0)
986+
else:
987+
click.echo(
988+
f"Error: No credentials found for the provider '{provider}' "
989+
f"inside '{dag.credentials_file_path}', cannot delete.",
990+
err=True,
991+
)
992+
sys.exit(-1)
993+
994+
if provider in dag.encrypted_credentials and not confirm_yes:
995+
confirm = ask_confirmation(
996+
f"The provider '{provider}' already have credentials saved inside '{dag.credentials_file_path}', "
997+
"do you want to override them?"
998+
)
999+
if not confirm:
1000+
sys.exit(1)
1001+
1002+
crypto = Crypto()
1003+
new_creds = {}
1004+
1005+
creds_from_cli = kwargs.pop("set")
1006+
if creds_from_cli:
1007+
for cred_def in creds_from_cli:
1008+
cred_name, *cred_parts = str(cred_def).split("=", 1)
1009+
cred = "".join(cred_parts)
1010+
if cred and cred_name in provider_credentials:
1011+
new_creds[cred_name] = crypto.encrypt(cred)
1012+
1013+
remaining = list(filter(lambda i: i not in new_creds, provider_credentials))
1014+
if remaining:
1015+
click.echo(
1016+
f"Error: Missing credentials field{'s' if len(remaining)>1 else ''}: {', '.join(remaining)}.",
1017+
err=True,
1018+
)
1019+
sys.exit(-1)
1020+
else:
1021+
click.echo(f"Enter your credentials for the provider '{provider}':")
1022+
for cred_name in provider_credentials:
1023+
cred_input = getpass.getpass(prompt=f"- {cred_name}: ")
1024+
new_creds[cred_name] = crypto.encrypt(cred_input)
1025+
1026+
set_provider_credentials_key(new_creds, crypto.key)
1027+
encrypted_credentials[provider] = new_creds
1028+
1029+
save_providers_credentials(dag.credentials_file_path, encrypted_credentials)
1030+
click.echo(
1031+
f"Credentials for '{provider}' saved inside '{dag.credentials_file_path}'."
1032+
)
1033+
1034+
8331035
if __name__ == "__main__":
8341036
eodag(obj={})

eodag/utils/cli.py

+23
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
# -*- coding: utf-8 -*-
2+
# Copyright 2021, CS GROUP - France, https://www.csgroup.eu/
3+
#
4+
# This file is part of EODAG project
5+
# https://www.github.com/CS-SI/EODAG
6+
#
7+
# Licensed under the Apache License, Version 2.0 (the "License");
8+
# you may not use this file except in compliance with the License.
9+
# You may obtain a copy of the License at
10+
#
11+
# http://www.apache.org/licenses/LICENSE-2.0
12+
#
13+
# Unless required by applicable law or agreed to in writing, software
14+
# distributed under the License is distributed on an "AS IS" BASIS,
15+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16+
# See the License for the specific language governing permissions and
17+
# limitations under the License.
18+
19+
20+
def ask_confirmation(question):
21+
"""Confirmation input for question"""
22+
ask = input(f"{question} [y/n] ").strip().lower()
23+
return ask in ["y", "Y"]

0 commit comments

Comments
 (0)