Bezpieczne przechowywanie zmiennych środowiskowych w GAE za pomocą app.yaml

100

Muszę przechowywać klucze API i inne poufne informacje app.yamljako zmienne środowiskowe do wdrożenia w GAE. Problem z tym polega na tym, że jeśli app.yamlwrzucę do GitHub, ta informacja stanie się publiczna (niedobra). Nie chcę przechowywać informacji w magazynie danych, ponieważ nie pasują one do projektu. Chciałbym raczej zamienić wartości z pliku, który jest wymieniony w .gitignorekażdym wdrożeniu aplikacji.

Oto mój plik app.yaml:

application: myapp
version: 3 
runtime: python27
api_version: 1
threadsafe: true

libraries:
- name: webapp2
  version: latest
- name: jinja2
  version: latest

handlers:
- url: /static
  static_dir: static

- url: /.*
  script: main.application  
  login: required
  secure: always
# auth_fail_action: unauthorized

env_variables:
  CLIENT_ID: ${CLIENT_ID}
  CLIENT_SECRET: ${CLIENT_SECRET}
  ORG: ${ORG}
  ACCESS_TOKEN: ${ACCESS_TOKEN}
  SESSION_SECRET: ${SESSION_SECRET}

Jakieś pomysły?

Ben
źródło
76
Chciałbym, aby GAE dodało opcję ustawiania zmiennych środowiska instancji za pośrednictwem konsoli programisty (jak każdy inny PaaS, z którym jestem zaznajomiony).
Hiszpania Pociąg
4
Możesz użyć magazynu danych. Zapoznaj się z tą odpowiedzią: stackoverflow.com/a/35254560/1027846
Mustafa İlhan
Rozwinięcie powyższego komentarza mustilica na temat korzystania z magazynu danych. Zobacz moją odpowiedź poniżej na kod, którego używam w moich projektach, aby to zrobić: stackoverflow.com/a/35261091#35261091 . W efekcie umożliwia edycję zmiennych środowiskowych z poziomu konsoli programisty, a wartości zastępcze są tworzone automatycznie.
Martin Omander
Dzięki mustilica i Martin. Właściwie od jakiegoś czasu używamy metody datastore i zgadzam się, że jest to najlepsze rozwiązanie tego problemu. Łatwiej zrobić z konfiguracją CI / CD niż podejście do pliku JSON, IMO.
Hiszpania Pociąg
1
2019 i GAE nadal nie rozwiązały tego problemu: /
Josh Noe

Odpowiedzi:

53

Jeśli są to dane wrażliwe, nie należy ich przechowywać w kodzie źródłowym, ponieważ zostaną one wpisane do kontroli źródła. Mogą go tam znaleźć niewłaściwe osoby (z Twojej organizacji lub spoza niej). Ponadto środowisko programistyczne prawdopodobnie używa innych wartości konfiguracyjnych niż środowisko produkcyjne. Jeśli te wartości są przechowywane w kodzie, będziesz musiał uruchamiać inny kod podczas programowania i produkcji, co jest niechlujnym i złym rozwiązaniem.

W moich projektach umieszczam dane konfiguracyjne w datastore przy użyciu tej klasy:

from google.appengine.ext import ndb

class Settings(ndb.Model):
  name = ndb.StringProperty()
  value = ndb.StringProperty()

  @staticmethod
  def get(name):
    NOT_SET_VALUE = "NOT SET"
    retval = Settings.query(Settings.name == name).get()
    if not retval:
      retval = Settings()
      retval.name = name
      retval.value = NOT_SET_VALUE
      retval.put()
    if retval.value == NOT_SET_VALUE:
      raise Exception(('Setting %s not found in the database. A placeholder ' +
        'record has been created. Go to the Developers Console for your app ' +
        'in App Engine, look up the Settings record with name=%s and enter ' +
        'its value in that record\'s value field.') % (name, name))
    return retval.value

Twoja aplikacja zrobiłaby to, aby uzyskać wartość:

API_KEY = Settings.get('API_KEY')

Jeśli istnieje wartość dla tego klucza w magazynie danych, otrzymasz ją. Jeśli go nie ma, zostanie utworzony rekord zastępczy i zostanie zgłoszony wyjątek. Wyjątek przypomni Ci, aby przejść do Developers Console i zaktualizować rekord zastępczy.

Uważam, że to eliminuje zgadywanie z ustawiania wartości konfiguracyjnych. Jeśli nie jesteś pewien, jakie wartości konfiguracyjne ustawić, po prostu uruchom kod, a on Ci powie!

Powyższy kod wykorzystuje bibliotekę ndb, która używa memcache i datastore pod maską, więc jest szybki.


Aktualizacja:

jelder zapytał, jak znaleźć wartości Datastore w konsoli App Engine i je ustawić. Oto jak:

  1. Wejdź na https://console.cloud.google.com/datastore/

  2. Wybierz swój projekt u góry strony, jeśli nie jest jeszcze wybrany.

  3. W menu rozwijanym Rodzaj wybierz opcję Ustawienia .

  4. Jeśli uruchomiłeś powyższy kod, twoje klucze pojawią się. Wszystkie będą miały wartość NIE USTAWIONĄ . Kliknij każdy z nich i ustaw jego wartość.

Mam nadzieję że to pomoże!

Twoje ustawienia utworzone przez klasę Settings

Kliknij aby edytowac

Wprowadź rzeczywistą wartość i zapisz

Martin Omander
źródło
2
Ze wszystkich udzielonych odpowiedzi wydaje się, że jest to najbliższe temu, jak Heroku radzi sobie ze sprawami. Będąc raczej nowym w GAE, nie bardzo rozumiem, gdzie w Developers Console znaleźć zastępczy rekord. Czy możesz wyjaśnić lub dla punktów bonusowych opublikować zrzuty ekranu?
jelder
2
dam ~… z całym szacunkiem dla gcloud, wygląda na to, że korzystanie z innej usługi dla tej konkretnej potrzeby jest bardzo złe. Poza tym Google zapewnia podejście „100% -herokuish” dla zmiennych środowiskowych w funkcjach firebase, ale nie dla funkcji gcloud (przynajmniej nieudokumentowane… jeśli się nie mylę)
Ben
1
Oto podsumowanie oparte na Twoim podejściu, które dodaje wyjątkowości i rezerwową zmienną środowiskową - gist.github.com/SpainTrain/6bf5896e6046a5d9e7e765d0defc8aa8
Hiszpania Train
3
Funkcje @Ben spoza Firebase obsługują zmienne środowiska (przynajmniej teraz).
NReilingh
3
@obl - aplikacja App Engine jest automatycznie uwierzytelniana we własnym magazynie danych, żadne szczegóły uwierzytelniania nie są potrzebne. Całkiem fajnie :-)
Martin Omander
56

To rozwiązanie jest proste, ale może nie pasować do wszystkich różnych zespołów.

Najpierw umieść zmienne środowiskowe w pliku env_variables.yaml , np.

env_variables:
  SECRET: 'my_secret'

Następnie umieść to env_variables.yamlwapp.yaml

includes:
  - env_variables.yaml

Na koniec dodaj env_variables.yamlto .gitignore, aby tajne zmienne nie istniały w repozytorium.

W takim przypadku env_variables.yamlpotrzeby należy podzielić między menedżerów wdrażania.

Shih-Wen Su
źródło
1
Aby dodać to, co dla niektórych może nie być oczywiste, zmienne środowiskowe zostałyby znalezione w process.env.MY_SECRET_KEYi jeśli potrzebujesz tych zmiennych środowiskowych w lokalnym środowisku deweloperskim, możesz użyć dotenvpakietu węzłów
Dave Kiss
2
Jak dotarłby env_variables.yamldo wszystkich instancji, to brakujący element układanki.
Christopher Oezbek
1
Ponadto: Jak używać tego lokalnie?
Christopher Oezbek
@ChristopherOezbek 1. Jak wdrożyć? Po prostu użyj gcloud app deploytak, jak zwykle do wdrażania w Google Cloud. 2. Jak lokalnie ustawić tajne zmienne środowiskowe? Jest wiele sposobów. Możesz po prostu użyć exportw wierszu polecenia lub użyć dowolnych narzędzi, takich jak sugerowany @DaveKiss.
Shih-Wen Su
To najprostsze rozwiązanie. Dostęp do sekretów można uzyskać w aplikacji za pośrednictwem os.environ.get('SECRET').
Quinn Comendant
19

Moje podejście polega na przechowywaniu kluczy klienta tylko w samej aplikacji App Engine. Klucze tajne klienta nie znajdują się w kontroli źródła ani na żadnych komputerach lokalnych. Ma to tę zaletę, że każdy współpracownik App Engine może wdrażać zmiany w kodzie bez martwienia się o sekrety klienta.

Przechowuję sekrety klienta bezpośrednio w Datastore i używam Memcache w celu zwiększenia opóźnienia dostępu do sekretów. Encje Datastore muszą zostać utworzone tylko raz i będą istnieć w przyszłych wdrożeniach. Oczywiście konsola App Engine może być używana do aktualizacji tych jednostek w dowolnym momencie.

Istnieją dwie możliwości wykonania jednorazowego tworzenia encji:

  • Użyj interaktywnej powłoki App Engine Remote API, aby utworzyć encje.
  • Utwórz procedurę obsługi tylko dla administratora, która zainicjuje jednostki z wartościami fikcyjnymi. Wywołaj ręcznie tę administracyjną procedurę obsługi, a następnie użyj konsoli App Engine, aby zaktualizować encje przy użyciu kluczy tajnych klienta produkcyjnego.
Bernd Verst
źródło
7
Wcale nie jest skomplikowane. Dzięki silnikowi aplikacji.
courtimas
18

To nie istniało, kiedy publikowałeś, ale dla każdego, kto się tu potknie, Google oferuje teraz usługę o nazwie Secret Manager .

To prosta usługa REST (oczywiście z pakietami SDK) do przechowywania Twoich sekretów w bezpiecznej lokalizacji na platformie Google Cloud. Jest to lepsze podejście niż magazyn danych, wymagające dodatkowych kroków w celu wyświetlenia przechowywanych sekretów i posiadające bardziej szczegółowy model uprawnień - w razie potrzeby możesz zabezpieczyć poszczególne sekrety w inny sposób dla różnych aspektów projektu.

Oferuje przechowywanie wersji, dzięki czemu można ze względną łatwością obsługiwać zmiany haseł, a także solidną warstwę zapytań i zarządzania, umożliwiającą w razie potrzeby odkrywanie i tworzenie sekretów w czasie wykonywania.

Python SDK

Przykładowe użycie:

from google.cloud import secretmanager_v1beta1 as secretmanager

secret_id = 'my_secret_key'
project_id = 'my_project'
version = 1    # use the management tools to determine version at runtime

client = secretmanager.SecretManagerServiceClient()

secret_path = client.secret_verion_path(project_id, secret_id, version)
response = client.access_secret_version(secret_path)
password_string = response.payload.data.decode('UTF-8')

# use password_string -- set up database connection, call third party service, whatever
Randolpho
źródło
3
To powinna być nowa poprawna odpowiedź. Secret Manager jest nadal w wersji Beta, ale jest to droga do przodu podczas pracy ze zmiennymi środowiskowymi.
King Leon
@KingLeon, czy użycie tego oznaczałoby konieczność refaktoryzacji kilku elementów os.getenv('ENV_VAR')?
Alejandro
Umieszczam kod podobny do powyższego w funkcji, a następnie używam czegoś takiego SECRET_KEY = env('SECRET_KEY', default=access_secret_version(GOOGLE_CLOUD_PROJECT_ID, 'SECRET_KEY', 1)). Ustawienie domyślneaccess_secret_version
King Leon
Ponadto używam django-environment. github.com/joke2k/django-environ
King Leon
Muszę zadać głupie pytanie, ale to, co nazywasz, secret_id = 'my_secret_key'nie jest wtedy obecne w twojej kontroli wersji?
dierre
16

Najlepszym sposobem, aby to zrobić, jest przechowywanie kluczy w pliku client_secrets.json i wykluczenie ich z przesyłania do git, umieszczając je w pliku .gitignore. Jeśli masz różne klucze dla różnych środowisk, możesz użyć interfejsu API app_identity, aby określić identyfikator aplikacji i odpowiednio załadować.

Jest tutaj dość obszerny przykład -> https://developers.google.com/api-client-library/python/guide/aaa_client_secrets .

Oto przykładowy kod:

# declare your app ids as globals ...
APPID_LIVE = 'awesomeapp'
APPID_DEV = 'awesomeapp-dev'
APPID_PILOT = 'awesomeapp-pilot'

# create a dictionary mapping the app_ids to the filepaths ...
client_secrets_map = {APPID_LIVE:'client_secrets_live.json',
                      APPID_DEV:'client_secrets_dev.json',
                      APPID_PILOT:'client_secrets_pilot.json'}

# get the filename based on the current app_id ...
client_secrets_filename = client_secrets_map.get(
    app_identity.get_application_id(),
    APPID_DEV # fall back to dev
    )

# use the filename to construct the flow ...
flow = flow_from_clientsecrets(filename=client_secrets_filename,
                               scope=scope,
                               redirect_uri=redirect_uri)

# or, you could load up the json file manually if you need more control ...
f = open(client_secrets_filename, 'r')
client_secrets = json.loads(f.read())
f.close()
Gwyn Howell
źródło
2
Zdecydowanie we właściwym kierunku, ale nie rozwiązuje to problemu zamiany wartości app.yamlpodczas wdrażania aplikacji. Jakieś pomysły?
Ben
1
Więc miej inny plik client_secrets dla każdego środowiska. Np. Client_secrets_live.json, client_secrets_dev.json, client_secrets_pilot.json itp., A następnie użyj logiki Pythona, aby określić, na którym serwerze jesteś i załaduj odpowiedni plik json. Metoda app_identity.get_application_id () może być przydatna do automatycznego wykrywania, na którym serwerze się znajdujesz. Czy to właśnie masz na myśli?
Gwyn Howell
@BenGrunfeld zobacz moją odpowiedź. Moje rozwiązanie robi dokładnie to. Nie rozumiem, jak ta odpowiedź rozwiązuje pytanie. Zakładam, że celem jest utrzymanie tajnej konfiguracji poza git i użycie git jako części wdrożenia. Tutaj ten plik nadal musi gdzieś być i zostać przesłany do procesu wdrażania. Może to być coś, co robisz w swojej aplikacji, ale możesz po prostu użyć technik, które zaznaczyłem, być może przechowując je w innym pliku, jeśli chcesz użyć tego w porównaniu z app.yaml. Jeśli rozumiem pytanie, jest to coś podobnego do wysyłania aplikacji open source z rzeczywistym sekretem klienta twórcy biblioteki lub produktem. klucz.
therewillbesnacks
1
Zajęło mi trochę czasu, aby to obejść, ale myślę, że jest to właściwe podejście. Nie mieszasz ustawień aplikacji ( app.yaml) z tajnymi kluczami i poufnymi informacjami, a naprawdę podoba mi się to, że używasz przepływu pracy Google do wykonania zadania. Dzięki @GwynHowell. =)
Ben
1
Podobnym podejściem byłoby umieszczenie tego pliku JSON w znanej lokalizacji w domyślnym zasobniku GCS aplikacji ( cloud.google.com/appengine/docs/standard/python/… ).
Hiszpania Pociąg
15

To rozwiązanie opiera się na przestarzałym pliku appcfg.py

Możesz użyć opcji wiersza poleceń -E w appcfg.py, aby skonfigurować zmienne środowiskowe podczas wdrażania aplikacji w GAE (aktualizacja appcfg.py)

$ appcfg.py
...
-E NAME:VALUE, --env_variable=NAME:VALUE
                    Set an environment variable, potentially overriding an
                    env_variable value from app.yaml file (flag may be
                    repeated to set multiple variables).
...
jla
źródło
Czy możesz przeszukiwać te zmienne środowiskowe gdzieś po wdrożeniu? (Mam nadzieję, że nie.)
Ztyx
Czy istnieje sposób na przekazanie zmiennych środowiskowych w ten sposób za pomocą gcloudnarzędzia?
Trevor
6

Większość odpowiedzi jest nieaktualna. Korzystanie z Google Cloud Datastore jest teraz nieco inne. https://cloud.google.com/python/getting-started/using-cloud-datastore

Oto przykład:

from google.cloud import datastore
client = datastore.Client()
datastore_entity = client.get(client.key('settings', 'TWITTER_APP_KEY'))
connection_string_prod = datastore_entity.get('value')

Zakłada się, że nazwa jednostki to „TWITTER_APP_KEY”, rodzaj to „ustawienia”, a „wartość” jest właściwością jednostki TWITTER_APP_KEY.

Jason F
źródło
3

Wygląda na to, że możesz zrobić kilka podejść. Mamy podobny problem i wykonujemy następujące czynności (dostosowane do Twojego przypadku użycia):

  • Utwórz plik, który przechowuje wszystkie dynamiczne wartości app.yaml i umieść go na bezpiecznym serwerze w środowisku kompilacji. Jeśli jesteś naprawdę paranoikiem, możesz asymetrycznie zaszyfrować wartości. Możesz nawet zachować to w prywatnym repozytorium, jeśli potrzebujesz kontroli wersji / dynamicznego ściągania, lub po prostu użyj skryptu powłoki, aby skopiować / wyciągnąć go z odpowiedniego miejsca.
  • Pobierz z git podczas wykonywania skryptu wdrażania
  • Po ściągnięciu git zmodyfikuj plik app.yaml, czytając i zapisując go w czystym Pythonie przy użyciu biblioteki yaml

Najłatwiej to zrobić, używając serwera ciągłej integracji, takiego jak Hudson , Bamboo lub Jenkins . Po prostu dodaj jakąś wtyczkę, krok skryptu lub przepływ pracy, który wykonuje wszystkie powyższe elementy, o których wspomniałem. Na przykład można przekazać zmienne środowiskowe skonfigurowane w samym Bamboo.

Podsumowując, wystarczy wcisnąć wartości podczas procesu kompilacji w środowisku, do którego masz tylko dostęp. Jeśli jeszcze nie automatyzujesz swoich kompilacji, powinieneś.

Inną opcją jest to, co powiedziałeś, umieść to w bazie danych. Jeśli powodem, dla którego tego nie robisz, jest to, że wszystko przebiega zbyt wolno, po prostu umieść wartości w memcache jako pamięć podręczną drugiej warstwy i przypnij wartości do instancji jako pamięć podręczną pierwszej warstwy. Jeśli wartości mogą się zmienić i musisz zaktualizować instancje bez ich ponownego uruchamiania, po prostu zachowaj skrót, aby wiedzieć, kiedy się zmieniają, lub w jakiś sposób wyzwalają, gdy coś, co robisz, zmienia wartości. To powinno być to.

tambesnacks
źródło
1
FWIW, to podejście jest najbardziej zgodne z czynnikiem konfiguracji zawartym w wytycznych aplikacji 12 Factor ( 12factor.net )
Hiszpania Pociąg
3

Powinieneś zaszyfrować zmienne za pomocą google kms i osadzić je w kodzie źródłowym. ( https://cloud.google.com/kms/ )

echo -n the-twitter-app-key | gcloud kms encrypt \
> --project my-project \
> --location us-central1 \
> --keyring THEKEYRING \
> --key THECRYPTOKEY \
> --plaintext-file - \
> --ciphertext-file - \
> | base64

umieść zaszyfrowaną (zaszyfrowaną i zakodowaną w formacie base64) wartość w zmiennej środowiskowej (w pliku yaml).

Trochę kodu w języku Python, aby rozpocząć odszyfrowywanie.

kms_client = kms_v1.KeyManagementServiceClient()
name = kms_client.crypto_key_path_path("project", "global", "THEKEYRING", "THECRYPTOKEY")

twitter_app_key = kms_client.decrypt(name, base64.b64decode(os.environ.get("TWITTER_APP_KEY"))).plaintext
Anders Elton
źródło
3

@Jason F za odpowiedź opiera się na wykorzystaniu Google DataStore jest blisko, ale kod jest nieco nieaktualne na podstawie przykładowego wykorzystania na docs bibliotecznych . Oto fragment, który zadziałał dla mnie:

from google.cloud import datastore

client = datastore.Client('<your project id>')
key = client.key('<kind e.g settings>', '<entity name>') # note: entity name not property
# get by key for this entity
result = client.get(key)
print(result) # prints all the properties ( a dict). index a specific value like result['MY_SECRET_KEY'])

Częściowo zainspirowany tym postem na Medium

kip2
źródło
2

Chciałem tylko zauważyć, jak rozwiązałem ten problem w javascript / nodejs. Do lokalnego rozwoju użyłem pakietu npm dotenv, który ładuje zmienne środowiskowe z pliku .env do process.env. Kiedy zacząłem używać GAE, dowiedziałem się, że zmienne środowiskowe muszą być ustawione w pliku „app.yaml”. Cóż, nie chciałem używać „dotenv” do lokalnego programowania i „app.yaml” do GAE (i powielać moje zmienne środowiskowe między dwoma plikami), więc napisałem mały skrypt, który ładuje zmienne środowiskowe app.yaml do procesu .env do lokalnego rozwoju. Mam nadzieję, że to komuś pomoże:

yaml_env.js:

(function () {
    const yaml = require('js-yaml');
    const fs = require('fs');
    const isObject = require('lodash.isobject')

    var doc = yaml.safeLoad(
        fs.readFileSync('app.yaml', 'utf8'), 
        { json: true }
    );

    // The .env file will take precedence over the settings the app.yaml file
    // which allows me to override stuff in app.yaml (the database connection string (DATABASE_URL), for example)
    // This is optional of course. If you don't use dotenv then remove this line:
    require('dotenv/config');

    if(isObject(doc) && isObject(doc.env_variables)) {
        Object.keys(doc.env_variables).forEach(function (key) {
            // Dont set environment with the yaml file value if it's already set
            process.env[key] = process.env[key] || doc.env_variables[key]
        })
    }
})()

Teraz umieść ten plik tak wcześnie, jak to możliwe w kodzie i gotowe:

require('../yaml_env')
gbruins
źródło
Czy nadal tak jest? Ponieważ używam .envpliku z tajnymi zmiennymi. Nie powielam ich w moim app.yamlpliku, a wdrożony kod nadal działa. Martwię się jednak, co stanie się z .envplikiem w chmurze. Czy zostanie zaszyfrowany czy coś? Jak mogę się upewnić, że nikt nie będzie miał dostępu do .envzmiennych pliku gcloud po jego wdrożeniu?
Gus
Nie jest to wcale potrzebne, ponieważ GAE automatycznie dodaje wszystkie zmienne zdefiniowane w pliku app.yaml do środowiska węzła. Zasadniczo jest to to samo, co robi dotenv ze zmiennymi zdefiniowanymi w pakiecie .env. Ale zastanawiam się, jak skonfigurować CD, ponieważ nie można przesłać app.yaml z vars env do VCS lub rurociągu ...
Jornve
1

Rozszerzam odpowiedź Martina

from google.appengine.ext import ndb

class Settings(ndb.Model):
    """
    Get sensitive data setting from DataStore.

    key:String -> value:String
    key:String -> Exception

    Thanks to: Martin Omander @ Stackoverflow
    https://stackoverflow.com/a/35261091/1463812
    """
    name = ndb.StringProperty()
    value = ndb.StringProperty()

    @staticmethod
    def get(name):
        retval = Settings.query(Settings.name == name).get()
        if not retval:
            raise Exception(('Setting %s not found in the database. A placeholder ' +
                             'record has been created. Go to the Developers Console for your app ' +
                             'in App Engine, look up the Settings record with name=%s and enter ' +
                             'its value in that record\'s value field.') % (name, name))
        return retval.value

    @staticmethod
    def set(name, value):
        exists = Settings.query(Settings.name == name).get()
        if not exists:
            s = Settings(name=name, value=value)
            s.put()
        else:
            exists.value = value
            exists.put()

        return True
JSBach
źródło
1

Istnieje pakiet pypi o nazwie gae_env, który umożliwia zapisywanie zmiennych środowiskowych appengine w Cloud Datastore. Pod maską wykorzystuje również Memcache, dzięki czemu jest szybki

Stosowanie:

import gae_env

API_KEY = gae_env.get('API_KEY')

Jeśli istnieje wartość dla tego klucza w magazynie danych, zostanie ona zwrócona. Jeśli go nie ma, __NOT_SET__zostanie utworzony rekord zastępczy i ValueNotSetErrorzostanie wyrzucony znak zapytania. Wyjątek przypomni Ci, aby przejść do Developers Console i zaktualizować rekord zastępczy.


Podobnie jak odpowiedź Martina, oto jak zaktualizować wartość klucza w Datastore:

  1. Przejdź do sekcji Datastore w konsoli programistów

  2. Wybierz swój projekt u góry strony, jeśli nie jest jeszcze wybrany.

  3. W menu rozwijanym Rodzaj wybierz GaeEnvSettings.

  4. Klucze, dla których zgłoszono wyjątek, będą miały wartość __NOT_SET__.

Twoje ustawienia utworzone przez klasę Settings

Kliknij aby edytowac

Wprowadź rzeczywistą wartość i zapisz


Przejdź do strony pakietu GitHub, aby uzyskać więcej informacji na temat użycia / konfiguracji

Książę Odame
źródło