Revolution1 / etcd3-py

Pure python client for etcd v3 (Using gRPC-JSON-Gateway)
Other
105 stars 25 forks source link

ErrUserEmpty error:'etcdserver: user name is empty, code:3' #109

Closed mckenziec closed 5 years ago

mckenziec commented 5 years ago

Description

Hi. Great looking python module. I'm trying to work out client etcdv3 usage with auth (certs + users) but when I try putting some keys in, I receive an error claiming my username is empty.

What I Did

I wrote up a bit of automated etcd scripting that creates an initial config, including users, basic key/dir, and sets up server/client auth. I don't know how much you'll want, but I'll start with my Python, and if they usage looks good, might scripts might help reproduce the problem.

from etcd3 import Client

ETCD_CLUSTER_PATH='/_cluster'
ETCD_NODE_PATH=ETCD_CLUSTER_PATH+'/${id}'
ETCD_PATH_SEP='/'
ETCD_KEY_MANIFEST='manifest'
ETCD_KEY_EFFECTIVE='effective'
ETCD_CLIENT_PROTO='https'
ETCD_CLIENT_HOST='127.0.0.1'
ETCD_CLIENT_USER='root'
ETCD_CLIENT_PASS='password'
ETCD_CLIENT_PORT=2379
ETCD_CLIENT_CERTS='/var/test/etcd/certs/'
ETCD_STATE_TTL=60

if __name__ == '__main__':
    try:
        cli = Client(
            host=ETCD_CLIENT_HOST, \
            port=ETCD_CLIENT_PORT, \
            protocol=ETCD_CLIENT_PROTO, \
            username=ETCD_CLIENT_USER, \
            password=ETCD_CLIENT_PASS, \
            cert=(ETCD_CLIENT_CERTS+'client.pem', ETCD_CLIENT_CERTS+'client-key.pem'), \
            verify=ETCD_CLIENT_CERTS+'ca.pem')
        cli.version()
        cli.put("/test","hi")
    except Exception, e:
        print >> sys.stderr, 'Error: ' + str(e)

Then I run it:

$ python etcd3_test.py 
Error: <ErrUserEmpty error:'etcdserver: user name is empty', code:3>

Are you assigning/passing the username argument through to the gRPC call?

Here's my etcd3_init.sh script:

#!/bin/bash
echo "etcd init script"
if [ "$#" -lt 3 ]; then
   echo "usage: `basename "$0"` <cert path> <connect ip> <connect port> <auth (opt)>"
   echo "example: ./`basename "$0"` /var/test/etcd/certs 127.0.0.1 2379"
   exit
fi
CERTS=$1
IP=$2
PORT=$3
AUTH=0
if [ "$#" -eq 4 ]; then
   AUTH=$4
fi

export ETCDCTL_API=3
ETCDCTL_HOSTS="--cacert=$CERTS/ca.pem --endpoints=https://$IP:$PORT"

ROOT_PASS=$(date +%s | sha256sum | base64 | head -c 32 ; echo)
ROOT_PASS="password"
PUB_PASS=$(date +%s | sha256sum | base64 | head -c 32 ; echo)
PRI_PASS=$(date +%s | sha256sum | base64 | head -c 64 | tail -c 32 ; echo)

etcdctl $ETCDCTL_HOSTS user add public:$PUB_PASS
etcdctl $ETCDCTL_HOSTS user add private:$PRI_PASS

etcdctl $ETCDCTL_HOSTS role add public_role
etcdctl $ETCDCTL_HOSTS role add private_role

etcdctl $ETCDCTL_HOSTS user grant-role public public_role
etcdctl $ETCDCTL_HOSTS user grant-role private private_role

etcdctl $ETCDCTL_HOSTS role grant-permission public_role --prefix=true readwrite /public
etcdctl $ETCDCTL_HOSTS role grant-permission private_role --prefix=true readwrite /public
etcdctl $ETCDCTL_HOSTS role grant-permission private_role --prefix=true readwrite /_cluster

etcdctl $ETCDCTL_HOSTS put /public/init "$(date)"
etcdctl $ETCDCTL_HOSTS get /public/init 
etcdctl $ETCDCTL_HOSTS put /_cluster/init "$(date)"

etcdctl $ETCDCTL_HOSTS user add root:$ROOT_PASS
etcdctl $ETCDCTL_HOSTS auth enable

if [ $AUTH ]; then
   echo "public:$PUB_PASS" > $AUTH
   echo "private:$PRI_PASS" >> $AUTH
   echo "root:$ROOT_PASS" >> $AUTH
fi
echo "public:$PUB_PASS"
echo "private:$PRI_PASS"
echo "root:$ROOT_PASS"

And here's the etcd.sh script I use to run the server:

#!/bin/bash

echo "etcd operation script"
if [ "$EUID" -ne 0 ]
  then echo "please run as root"
  exit
fi
if [ "$#" -ne 4 ]; then
   echo "usage: `basename "$0"` <cert path> <data path> <inf> <listen port>"
   echo "example: ./`basename "$0"` /var/test/etcd/certs /var/test/etcd/data eth0 2379"
   exit
fi

DATA=$2
CERTS=$1
INF=$3
#IP=$3
PORT=$4
NEW=true
if [ -d $DATA ]; then
   NEW=false
fi

echo "using: data=$DATA certs=$CERTS ip=$IP port=$PORT"
HOST=$(hostname)
IP=$(ip -4 addr show $INF | grep -oP '(?<=inet\s)\d+(\.\d+){3}')

if [ $NEW = true ]; then
   rm -rf ./auth
   bash -c "sleep 3; ./etcd_init.sh $CERTS 127.0.0.1 2379 ./auth" &
fi

etcd --enable-v2=false --data-dir $DATA --name $HOST --trusted-ca-file=$CERTS/ca.pem --cert-file=$CERTS/server.pem --key-file=$CERTS/server-key.pem --listen-client-urls=https://127.0.0.1:$PORT,https://$IP:$PORT --advertise-client-urls=https://127.0.0.1:$PORT,https://$IP:$PORT,https://$(hostname):$PORT,https://$(hostname -s):$PORT
issue-label-bot[bot] commented 5 years ago

Issue-Label Bot is automatically applying the label bug to this issue, with a confidence of 0.60. Please mark this comment with :thumbsup: or :thumbsdown: to give our bot feedback!

Links: app homepage, dashboard and code for this bot.

mckenziec commented 5 years ago

Ok, I found in the Client code the auth() method, which I have to call myself before any range/put type operations. Maybe the docs need updating, or the Client constructor needs to include the auth operation.

from etcd3 import Client

ETCD_CLUSTER_PATH='/_cluster'
ETCD_NODE_PATH=ETCD_CLUSTER_PATH+'/${id}'
ETCD_PATH_SEP='/'
ETCD_KEY_MANIFEST='manifest'
ETCD_KEY_EFFECTIVE='effective'
ETCD_CLIENT_PROTO='https'
ETCD_CLIENT_HOST='127.0.0.1'
ETCD_CLIENT_USER='root'
ETCD_CLIENT_PASS='password'
ETCD_CLIENT_PORT=2379
ETCD_CLIENT_CERTS='/var/test/etcd/certs/'
ETCD_STATE_TTL=60

if __name__ == '__main__':
    try:
        cli = Client(
            host=ETCD_CLIENT_HOST, \
            port=ETCD_CLIENT_PORT, \
            protocol=ETCD_CLIENT_PROTO, \
            cert=(ETCD_CLIENT_CERTS+'client.pem', ETCD_CLIENT_CERTS+'client-key.pem'), \
            verify=ETCD_CLIENT_CERTS+'ca.pem')
        cli.auth(username=ETCD_CLIENT_USER, password=ETCD_CLIENT_PASS)
        cli.version()
        cli.put("/test","hi")
    except Exception, e:
        print >> sys.stderr, 'Error: ' + str(e)
mckenziec commented 5 years ago

Ah... I see maybe why auth is separate. I was debugging my test app and stepped away, when I tried to use the Client object to range() I received an invalid token error:

*** ErrInvalidAuthToken: <ErrInvalidAuthToken error:'etcdserver: invalid auth token', code:16>

Is there a way to check if the token is still valid? Or should I just catch the exception and design a re-auth retry?

Does this affect watch()? I was going to play with it next.

Revolution1 commented 5 years ago

https://github.com/Revolution1/etcd3-py#faq

Since the most use case dosen't enable etcd's auth (from my experience) and it still need some manual process when auth fail, I did't make authenticate be auto.

You should catch the error your self. (If you want to make it auto, just inherit the etcd3.Client and override call_rpc and __init__ method)

mckenziec commented 5 years ago

Thanks the replying and sorry for not spotting the previous issue in the faq. I would have looked harder but I spotted the username and password arguments in the Client class. I would suggest removing them if auth() has to be called manually. Just a suggestion.

Also, FYI, I'm using auth support so I can have key paths which are effectively "public" and "private". e.g. public/private users/roles, granted permissions to different paths. It's a use case that some might not appreciate, but if you want to design an automated request/approval process to grant a client access to your private keys, it means you can try to do it entirely with etcd and not some external mechanism to get the user/pass for the private keys.

Thanks.

Revolution1 commented 5 years ago

I pinned the issue to make sure people notice this.

Yeah, it is likely a API design mistake that I put password and username to Client.__init__. But removing may considered as a "breaking change".

I will remove it in the next major release.