Inleiding tot de Kubernetes-autorisatie van Hashicorp Consul

Inleiding tot de Kubernetes-autorisatie van Hashicorp Consul

Dat klopt, na vrijlating Hashicorp-consul 1.5.0 Begin mei 2019 kunt u in Consul applicaties en services autoriseren die native in Kubernetes draaien.

In deze tutorial gaan we stap voor stap creëren POC (Proof of concept, PoC) waarin deze nieuwe functie wordt gedemonstreerd. Er wordt van je verwacht dat je basiskennis hebt van Kubernetes en Hashicorp's Consul. Hoewel je elk cloudplatform of lokale omgeving kunt gebruiken, gebruiken we in deze tutorial het Cloud Platform van Google.

Recensie

Als wij naar Consuldocumentatie over de autorisatiemethode, krijgen we een snel overzicht van het doel en de gebruikssituatie ervan, evenals enkele technische details en een algemeen overzicht van de logica. Ik raad je ten zeerste aan om het minstens één keer te lezen voordat je verdergaat, aangezien ik het nu allemaal zal uitleggen en erop zal kauwen.

Inleiding tot de Kubernetes-autorisatie van Hashicorp Consul

Diagram 1: Officieel overzicht van de Consul-autorisatiemethode

Laten we naar binnen kijken documentatie voor een specifieke Kubernetes-autorisatiemethode.

Natuurlijk staat daar nuttige informatie, maar er is geen handleiding over hoe je deze allemaal daadwerkelijk kunt gebruiken. Dus, zoals ieder weldenkend mens, struin je het internet af op zoek naar begeleiding. En dan... Je faalt. Het gebeurt. Laten we dit oplossen.

Voordat we verder gaan met het maken van onze POC, gaan we terug naar het overzicht van de autorisatiemethoden van Consul (diagram 1) en verfijnen we dit in de context van Kubernetes.

Architectuur

In deze zelfstudie maken we een Consul-server op een aparte machine die zal communiceren met een Kubernetes-cluster waarop de Consul-client is geïnstalleerd. Vervolgens maken we onze dummy-applicatie in de pod en gebruiken we onze geconfigureerde autorisatiemethode om te lezen uit onze Consul-sleutel/waarde-opslag.

Het onderstaande diagram beschrijft de architectuur die we in deze tutorial maken, evenals de logica achter de autorisatiemethode, die later zal worden uitgelegd.

Inleiding tot de Kubernetes-autorisatie van Hashicorp Consul

Diagram 2: Overzicht van de Kubernetes-autorisatiemethode

Een korte opmerking: de Consul-server hoeft niet buiten het Kubernetes-cluster te staan ​​om dit te laten werken. Maar ja, hij kan het zo en zo doen.

Als we dus het Consul-overzichtsdiagram (diagram 1) nemen en Kubernetes erop toepassen, krijgen we het bovenstaande diagram (diagram 2), en de logica hier is als volgt:

  1. Aan elke pod is een serviceaccount gekoppeld met een JWT-token dat is gegenereerd en bekend is bij Kubernetes. Dit token wordt ook standaard in de pod ingevoegd.
  2. Onze applicatie of service in de pod initieert een inlogopdracht voor onze Consul-client. Het inlogverzoek bevat ook onze token en naam speciaal gemaakt autorisatiemethode (Kubernetes-type). Deze stap #2 komt overeen met stap 1 van het Consul-diagram (schema 1).
  3. Onze Consul-client stuurt dit verzoek vervolgens door naar onze Consul-server.
  4. MAGIE! Hier verifieert de Consul-server de authenticiteit van het verzoek, verzamelt informatie over de identiteit van het verzoek en vergelijkt deze met eventuele bijbehorende vooraf gedefinieerde regels. Hieronder vindt u nog een diagram om dit te illustreren. Deze stap komt overeen met de stappen 3, 4 en 5 van het Consul-overzichtsdiagram (diagram 1).
  5. Onze Consul-server genereert een Consul-token met machtigingen volgens onze gespecificeerde autorisatiemethoderegels (die we hebben gedefinieerd) met betrekking tot de identiteit van de aanvrager. Vervolgens wordt het token teruggestuurd. Dit komt overeen met stap 6 van het Consul-diagram (diagram 1).
  6. Onze Consul-client stuurt het token door naar de aanvragende applicatie of dienst.

Onze applicatie of service kan dit Consul-token nu gebruiken om te communiceren met onze Consul-gegevens, zoals bepaald door de rechten van het token.

De magie wordt onthuld!

Voor degenen onder jullie die niet blij zijn met zomaar een konijn uit de hoge hoed en willen weten hoe het werkt... laat me je "laten zien hoe diep konijnenhol.

Zoals eerder vermeld, is onze "magische" stap (Afbeelding 2: Stap 4) de plaats waar de Consul-server het verzoek verifieert, informatie over het verzoek verzamelt en deze vergelijkt met eventuele bijbehorende vooraf gedefinieerde regels. Deze stap komt overeen met de stappen 3, 4 en 5 van het Consul-overzichtsdiagram (diagram 1). Hieronder ziet u een diagram (diagram 3), waarvan het doel is om duidelijk te laten zien wat er feitelijk gebeurt onder de motorkap specifieke Kubernetes-autorisatiemethode.

Inleiding tot de Kubernetes-autorisatie van Hashicorp Consul

Diagram 3: De magie wordt onthuld!

  1. Als uitgangspunt stuurt onze Consul-client het inlogverzoek door naar onze Consul-server met het Kubernetes-accounttoken en de specifieke instantienaam van de eerder gemaakte autorisatiemethode. Deze stap komt overeen met stap 3 in de vorige circuituitleg.
  2. Nu moet de Consul-server (of leider) de authenticiteit van het ontvangen token verifiëren. Daarom zal het het Kubernetes-cluster raadplegen (via de Consul-client) en, met de juiste machtigingen, ontdekken of het token echt is en van wie het is.
  3. Het gevalideerde verzoek wordt vervolgens teruggestuurd naar de Consul-leider, en de Consul-server zoekt de instantie van de autorisatiemethode op met de opgegeven naam uit het inlogverzoek (en het Kubernetes-type).
  4. De consulleider identificeert het gespecificeerde exemplaar van de autorisatiemethode (indien gevonden) en leest de reeks bindende regels die eraan zijn gekoppeld. Vervolgens leest het deze regels en vergelijkt deze met de geverifieerde identiteitskenmerken.
  5. TA-dah! Laten we verder gaan met stap 5 in de vorige circuituitleg.

Voer Consul-server uit op een gewone virtuele machine

Vanaf nu zal ik vooral instructies geven over het maken van deze POC, vaak in opsommingstekens, zonder volledige zinsuitleg. Zoals eerder opgemerkt, zal ik GCP ook gebruiken om alle infrastructuur te creëren, maar je kunt dezelfde infrastructuur ergens anders creëren.

  • Start de virtuele machine (instance/server).

Inleiding tot de Kubernetes-autorisatie van Hashicorp Consul

  • Maak een regel voor de firewall (beveiligingsgroep in AWS):
  • Ik wijs graag dezelfde machinenaam toe aan zowel de regel als de netwerktag, in dit geval "skywiz-consul-server-poc".
  • Zoek het IP-adres van uw lokale computer en voeg het toe aan de lijst met bron-IP-adressen, zodat we toegang krijgen tot de gebruikersinterface (UI).
  • Open poort 8500 voor gebruikersinterface. Klik op Maken. We zullen deze firewall binnenkort opnieuw wijzigen [link].
  • Voeg een firewallregel toe aan de instantie. Ga terug naar het VM-dashboard op Consul Server en voeg “skywiz-consul-server-poc” toe aan het veld met netwerktags. Klik op Opslaan.

Inleiding tot de Kubernetes-autorisatie van Hashicorp Consul

  • Installeer Consul op een virtuele machine, kijk hier. Houd er rekening mee dat u Consul-versie ≥ 1.5 nodig heeft [link]
  • Laten we een Consul met één knooppunt maken - de configuratie is als volgt.

groupadd --system consul
useradd -s /sbin/nologin --system -g consul consul
mkdir -p /var/lib/consul
chown -R consul:consul /var/lib/consul
chmod -R 775 /var/lib/consul
mkdir /etc/consul.d
chown -R consul:consul /etc/consul.d

  • Voor een meer gedetailleerde handleiding over het installeren van Consul en het opzetten van een cluster van 3 knooppunten, zie hier.
  • Maak als volgt een bestand /etc/consul.d/agent.json [link]:

### /etc/consul.d/agent.json
{
 "acl" : {
 "enabled": true,
 "default_policy": "deny",
 "enable_token_persistence": true
 }
}

  • Start onze Consul-server:

consul agent 
-server 
-ui 
-client 0.0.0.0 
-data-dir=/var/lib/consul 
-bootstrap-expect=1 
-config-dir=/etc/consul.d

  • Je zou een heleboel uitvoer moeten zien en eindigen met "... update geblokkeerd door ACL's."
  • Zoek het externe IP-adres van de Consul-server en open een browser met dit IP-adres op poort 8500. Zorg ervoor dat de gebruikersinterface wordt geopend.
  • Probeer een sleutel/waarde-paar toe te voegen. Er moet sprake zijn van een vergissing. Dit komt omdat we de Consul-server hebben geladen met een ACL en alle regels hebben uitgeschakeld.
  • Ga terug naar je shell op de Consul-server en start het proces op de achtergrond of op een andere manier om het draaiende te krijgen en voer het volgende in:

consul acl bootstrap

  • Zoek de waarde 'SecretID' en keer terug naar de gebruikersinterface. Voer op het tabblad ACL de geheime ID in van het token dat u zojuist hebt gekopieerd. Kopieer SecretID ergens anders, we hebben het later nodig.
  • Voeg nu een sleutel/waarde-paar toe. Voeg voor deze POC het volgende toe: key: “custom-ns/test_key”, waarde: “Ik sta in de map custom-ns!”

Het lanceren van een Kubernetes-cluster voor onze applicatie met de Consul-client als Daemonset

  • Maak een K8s-cluster (Kubernetes). We creëren het in dezelfde zone als de server voor snellere toegang, zodat we hetzelfde subnet kunnen gebruiken om eenvoudig verbinding te maken met interne IP-adressen. We noemen het "skywiz-app-met-consul-client-poc".

Inleiding tot de Kubernetes-autorisatie van Hashicorp Consul

  • Even terzijde: hier is een goede tutorial die ik tegenkwam tijdens het opzetten van een POC Consul-cluster met Consul Connect.
  • We zullen ook de Hashicorp-helmgrafiek gebruiken met een uitgebreid waardenbestand.
  • Installeer en configureer Helm. Configuratiestappen:

kubectl create serviceaccount tiller --namespace kube-system
kubectl create clusterrolebinding tiller-admin-binding 
   --clusterrole=cluster-admin --serviceaccount=kube-system:tiller
./helm init --service-account=tiller
./helm update

### poc-helm-consul-values.yaml
global:
 enabled: false
 image: "consul:latest"
# Expose the Consul UI through this LoadBalancer
ui:
 enabled: false
# Allow Consul to inject the Connect proxy into Kubernetes containers
connectInject:
 enabled: false
# Configure a Consul client on Kubernetes nodes. GRPC listener is required for Connect.
client:
 enabled: true
 join: ["<PRIVATE_IP_CONSUL_SERVER>"]
 extraConfig: |
{
  "acl" : {
 "enabled": true,   
 "default_policy": "deny",   
 "enable_token_persistence": true 
  }
}
# Minimal Consul configuration. Not suitable for production.
server:
 enabled: false
# Sync Kubernetes and Consul services
syncCatalog:
 enabled: false

  • Roergrafiek toepassen:

./helm install -f poc-helm-consul-values.yaml ./consul-helm - name skywiz-app-with-consul-client-poc

  • Wanneer het probeert te starten, heeft het machtigingen nodig voor de Consul-server, dus laten we deze toevoegen.
  • Let op het “Pod-adresbereik” op het clusterdashboard en raadpleeg onze “skywiz-consul-server-poc” firewallregel.
  • Voeg het adresbereik voor de pod toe aan de lijst met IP-adressen en open poorten 8301 en 8300.

Inleiding tot de Kubernetes-autorisatie van Hashicorp Consul

  • Ga naar de Consul UI en na een paar minuten ziet u ons cluster verschijnen op het knooppuntentabblad.

Inleiding tot de Kubernetes-autorisatie van Hashicorp Consul

Een autorisatiemethode configureren door Consul te integreren met Kubernetes

  • Keer terug naar de Consul-servershell en exporteer het token dat u eerder hebt opgeslagen:

export CONSUL_HTTP_TOKEN=<SecretID>

  • We hebben informatie nodig van ons Kubernetes-cluster om een ​​exemplaar van de auth-methode te maken:
  • kubernetes-host

kubectl get endpoints | grep kubernetes

  • kubernetes-service-account-jwt

kubectl get sa <helm_deployment_name>-consul-client -o yaml | grep "- name:"
kubectl get secret <secret_name_from_prev_command> -o yaml | grep token:

  • Het token is base64-gecodeerd, dus ontsleutel het met uw favoriete tool [link]
  • kubernetes-ca-cert

kubectl get secret <secret_name_from_prev_command> -o yaml | grep ca.crt:

  • Neem het “ca.crt”-certificaat (na base64-decodering) en schrijf het in het “ca.crt”-bestand.
  • Instantieer nu de auth-methode en vervang de tijdelijke aanduidingen door de waarden die u zojuist hebt ontvangen.

consul acl auth-method create 
-type "kubernetes" 
-name "auth-method-skywiz-consul-poc" 
-description "This is an auth method using kubernetes for the cluster skywiz-app-with-consul-client-poc" 
-kubernetes-host "<k8s_endpoint_retrieved earlier>" 
[email protected] 
-kubernetes-service-account-
jwt="<decoded_token_retrieved_earlier>"

  • Vervolgens moeten we een regel maken en deze aan de nieuwe rol koppelen. Voor dit deel kunt u Consul UI gebruiken, maar wij gebruiken de opdrachtregel.
  • Schrijf een regel

### kv-custom-ns-policy.hcl
key_prefix "custom-ns/" {
 policy = "write"
}

  • Pas de regel toe

consul acl policy create 
-name kv-custom-ns-policy 
-description "This is an example policy for kv at custom-ns/" 
-rules @kv-custom-ns-policy.hcl

  • Zoek de ID van de regel die u zojuist hebt gemaakt op basis van de uitvoer.
  • Maak een rol met een nieuwe regel.

consul acl role create 
-name "custom-ns-role" 
-description "This is an example role for custom-ns namespace" 
-policy-id <policy_id>

consul acl binding-rule create 
-method=auth-method-skywiz-consul-poc 
-bind-type=role 
-bind-name='custom-ns-role' 
-selector='serviceaccount.namespace=="custom-ns"'

Tenslotte configuraties

Toegangsrechten

  • Toegangsrechten aanmaken. We moeten Consul toestemming geven om de identiteit van het K8s-serviceaccounttoken te verifiëren en te identificeren.
  • Schrijf het volgende naar het bestand [koppeling]:

###skywiz-poc-consul-server_rbac.yaml
---
kind: ClusterRoleBinding
apiVersion: rbac.authorization.k8s.io/v1
metadata:
 name: review-tokens
 namespace: default
subjects:
- kind: ServiceAccount
 name: skywiz-app-with-consul-client-poc-consul-client
 namespace: default
roleRef:
 kind: ClusterRole
 name: system:auth-delegator
 apiGroup: rbac.authorization.k8s.io
---
kind: ClusterRole
apiVersion: rbac.authorization.k8s.io/v1
metadata:
 name: service-account-getter
 namespace: default
rules:
- apiGroups: [""]
 resources: ["serviceaccounts"]
 verbs: ["get"]
---
kind: ClusterRoleBinding
apiVersion: rbac.authorization.k8s.io/v1
metadata:
 name: get-service-accounts
 namespace: default
subjects:
- kind: ServiceAccount
 name: skywiz-app-with-consul-client-poc-consul-client
 namespace: default
roleRef:
 kind: ClusterRole
 name: service-account-getter
 apiGroup: rbac.authorization.k8s.io

  • Laten we toegangsrechten creëren

kubectl create -f skywiz-poc-consul-server_rbac.yaml

Verbinding maken met Consul-client

  • Zoals opgemerkt hierEr zijn verschillende opties om verbinding te maken met daemonset, maar we gaan verder met de volgende eenvoudige oplossing:
  • Pas het volgende bestand toe [link].

### poc-consul-client-ds-svc.yaml
apiVersion: v1
kind: Service
metadata:
 name: consul-ds-client
spec:
 selector:
   app: consul
   chart: consul-helm
   component: client
   hasDNS: "true"
   release: skywiz-app-with-consul-client-poc
 ports:
 - protocol: TCP
   port: 80
   targetPort: 8500

  • Gebruik vervolgens de volgende ingebouwde opdracht om een ​​configmap [link]. Let op: wij verwijzen naar de naam van onze dienst, vervang deze indien nodig.

cat <<EOF | kubectl apply -f -
apiVersion: v1
kind: ConfigMap
metadata:
 labels:
   addonmanager.kubernetes.io/mode: EnsureExists
 name: kube-dns
 namespace: kube-system
data:
 stubDomains: |
   {"consul": ["$(kubectl get svc consul-ds-client -o jsonpath='{.spec.clusterIP}')"]}
EOF

Het testen van de authenticatiemethode

Laten we nu de magie in actie zien!

  • Maak meerdere sleutelmappen aan met dezelfde sleutel op het hoogste niveau (d.w.z. /sample_key) en een waarde naar keuze. Creëer het juiste beleid en de juiste rollen voor nieuwe sleutelpaden. De bindingen doen we later.

Inleiding tot de Kubernetes-autorisatie van Hashicorp Consul

Aangepaste naamruimtetest:

  • Laten we onze eigen naamruimte maken:

kubectl create namespace custom-ns

  • Laten we een pod maken in onze nieuwe naamruimte. Schrijf de configuratie voor de pod.

###poc-ubuntu-custom-ns.yaml
apiVersion: v1
kind: Pod
metadata:
 name: poc-ubuntu-custom-ns
 namespace: custom-ns
spec:
 containers:
 - name: poc-ubuntu-custom-ns
   image: ubuntu
   command: ["/bin/bash", "-ec", "sleep infinity"]
 restartPolicy: Never

  • Creëer onder:

kubectl create -f poc-ubuntu-custom-ns.yaml

  • Zodra de container draait, ga je daarheen en installeer je curl.

kubectl exec poc-ubuntu-custom-ns -n custom-ns -it /bin/bash
apt-get update && apt-get install curl -y

  • Nu sturen we een inlogverzoek naar Consul met behulp van de autorisatiemethode die we eerder hebben gemaakt [link].
  • Om het ingevoerde token van uw serviceaccount te bekijken:

cat /run/secrets/kubernetes.io/serviceaccount/token

  • Schrijf het volgende naar een bestand in de container:

### payload.json
{
 "AuthMethod": "auth-method-test",
 "BearerToken": "<jwt_token>"
}

  • Log in!

curl 
--request POST 
--data @payload.json 
consul-ds-client.default.svc.cluster.local/v1/acl/login

  • Om de bovenstaande stappen in één regel uit te voeren (aangezien we meerdere tests uitvoeren), kunt u het volgende doen:

echo "{ 
"AuthMethod": "auth-method-skywiz-consul-poc", 
"BearerToken": "$(cat /run/secrets/kubernetes.io/serviceaccount/token)" 
}" 
| curl 
--request POST 
--data @- 
consul-ds-client.default.svc.cluster.local/v1/acl/login

  • Werken! Dat zou tenminste moeten. Neem nu de SecretID en probeer toegang te krijgen tot de sleutel/waarde waartoe we toegang zouden moeten hebben.

curl 
consul-ds-client.default.svc.cluster.local/v1/kv/custom-ns/test_key --header “X-Consul-Token: <SecretID_from_prev_response>”

  • U kunt base64 "Value" decoderen en zien dat deze overeenkomt met de waarde in custom-ns/test_key in de gebruikersinterface. Als u dezelfde waarde hierboven in deze zelfstudie hebt gebruikt, is uw gecodeerde waarde IkknbSBpbiB0aGUgY3VzdG9tLW5zIGZvbGRlciEi.

Gebruikersserviceaccounttest:

  • Maak een aangepast ServiceAccount met de volgende opdracht [link].

kubectl apply -f - <<EOF
apiVersion: v1
kind: ServiceAccount
metadata:
 name: custom-sa
EOF

  • Maak een nieuw configuratiebestand voor de pod. Houd er rekening mee dat ik de krulinstallatie heb meegeleverd om arbeid te besparen :)

###poc-ubuntu-custom-sa.yaml
apiVersion: v1
kind: Pod
metadata:
 name: poc-ubuntu-custom-sa
 namespace: default
spec:
 serviceAccountName: custom-sa
 containers:
 - name: poc-ubuntu-custom-sa
   image: ubuntu
   command: ["/bin/bash","-ec"]
   args: ["apt-get update && apt-get install curl -y; sleep infinity"]
 restartPolicy: Never

  • Laat daarna een schaal in de container lopen.

kubectl exec -it poc-ubuntu-custom-sa /bin/bash

  • Log in!

echo "{ 
"AuthMethod": "auth-method-skywiz-consul-poc", 
"BearerToken": "$(cat /run/secrets/kubernetes.io/serviceaccount/token)" 
}" 
| curl 
--request POST 
--data @- 
consul-ds-client.default.svc.cluster.local/v1/acl/login

  • Geen toestemming. Oh, we zijn vergeten nieuwe regels toe te voegen die bindend zijn met de juiste machtigingen, laten we dat nu doen.

Herhaal de voorgaande stappen hierboven:
a) Creëer een identiek beleid voor het voorvoegsel “custom-sa/”.
b) Creëer een rol, noem deze “custom-sa-role”
c) Koppel het beleid aan de rol.

  • Maak een Rule-Binding (alleen mogelijk vanuit cli/api). Let op de verschillende betekenis van de selectievlag.

consul acl binding-rule create 
-method=auth-method-skywiz-consul-poc 
-bind-type=role 
-bind-name='custom-sa-role' 
-selector='serviceaccount.name=="custom-sa"'

  • Meld u opnieuw aan vanuit de container "poc-ubuntu-custom-sa". Succes!
  • Bekijk onze toegang tot het custom-sa/key pad.

curl 
consul-ds-client.default.svc.cluster.local/v1/kv/custom-sa/test_key --header “X-Consul-Token: <SecretID>”

  • U kunt er ook voor zorgen dat dit token geen toegang verleent tot kv in "custom-ns/". Herhaal gewoon de bovenstaande opdracht nadat u "custom-sa" hebt vervangen door het voorvoegsel "custom-ns".
    Toestemming geweigerd.

Overlay-voorbeeld:

  • Het is vermeldenswaard dat alle regelbindende toewijzingen met deze rechten aan het token worden toegevoegd.
  • Onze container "poc-ubuntu-custom-sa" bevindt zich in de standaardnaamruimte - dus laten we deze gebruiken voor een andere regelbinding.
  • Herhaal voorgaande stappen:
    a) Maak een identiek beleid voor het sleutelvoorvoegsel “default/”.
    b) Maak een rol, noem deze “default-ns-role”
    c) Koppel het beleid aan de rol.
  • Maak een Rule-Binding (alleen mogelijk vanuit cli/api)

consul acl binding-rule create 
-method=auth-method-skywiz-consul-poc 
-bind-type=role 
-bind-name='default-ns-role' 
-selector='serviceaccount.namespace=="default"'

  • Ga terug naar onze "poc-ubuntu-custom-sa" container en probeer toegang te krijgen tot het "default/" kv-pad.
  • Toestemming geweigerd.
    U kunt de opgegeven referenties voor elk token bekijken in de gebruikersinterface onder ACL > Tokens. Zoals u kunt zien, is er aan ons huidige token slechts één “custom-sa-role” gekoppeld. Het token dat we momenteel gebruiken, werd gegenereerd toen we inlogden en er was toen maar één regelbinding die overeenkwam. We moeten opnieuw inloggen en het nieuwe token gebruiken.
  • Zorg ervoor dat u zowel de kv-paden "custom-sa/" als "default/" kunt lezen.
    Succes!
    Dit komt omdat onze “poc-ubuntu-custom-sa” overeenkomt met de “custom-sa” en “default-ns” regelbindingen.

Conclusie

TTL-tokenbeheer?

Op het moment dat dit artikel wordt geschreven, is er geen geïntegreerde manier om de TTL te bepalen voor tokens die door deze autorisatiemethode worden gegenereerd. Het zou een fantastische kans zijn om veilige automatisering van Consul-autorisatie te bieden.

Er is een optie om handmatig een token aan te maken met TTL:

Hopelijk kunnen we in de nabije toekomst bepalen hoe tokens worden gegenereerd (per regel of autorisatiemethode) en TTL toevoegen.

Tot die tijd wordt u aangeraden een uitlogeindpunt in uw logica te gebruiken.

Lees ook andere artikelen op onze blog:

Bron: www.habr.com

Voeg een reactie