Skip to content

Commit

Permalink
Auth changes for 2.2-M04
Browse files Browse the repository at this point in the history
  • Loading branch information
technige committed Feb 16, 2015
1 parent c4c0aed commit 3da8610
Show file tree
Hide file tree
Showing 7 changed files with 100 additions and 124 deletions.
6 changes: 4 additions & 2 deletions bau
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ LATEST_1_8_VERSION="1.8.3"
LATEST_1_9_VERSION="1.9.9"
LATEST_2_0_VERSION="2.0.4"
LATEST_2_1_VERSION="2.1.6"
LATEST_2_2_VERSION="2.2.0-M01"
LATEST_2_2_VERSION="2.2.0-M04"
LATEST_VERSION=$LATEST_2_2_VERSION

SELF=$0
Expand Down Expand Up @@ -71,8 +71,10 @@ function test {
mkdir -p "$LOG/$NEO4J_VERSION" 2> /dev/null
trap neo4j_stop_and_exit SIGINT SIGTERM
neo4j_start
NEW_PASSWORD="password"
python -m py2neo.password neo4j neo4j $NEW_PASSWORD 2> /dev/null
export NEO4J_HOME="$HOME"
export NEO4J_AUTH_TOKEN=$(python -m py2neo.password neo4j neo4j password)
export NEO4J_AUTH="neo4j:$NEW_PASSWORD"
if [ "$1" == "" ]
then
py.test -vx --cov-config .coveragerc --cov py2neo --cov-report term-missing --cov-report html test/core
Expand Down
9 changes: 0 additions & 9 deletions book/cookbook.rst
Original file line number Diff line number Diff line change
Expand Up @@ -38,15 +38,6 @@ Escaping Values in Cypher
'CREATE (a)-[ab:`KNOWS WELL`]->(b) RETURN ab'


Supplying a User Name and Password
==================================

This form of authentication can be used if a database server is behind (for example) an Apache
proxy. It is not the same as the authentication mechanism bundled with Neo4j 2.2 and above.

.. autofunction:: py2neo.authenticate


URI Rewriting
=============

Expand Down
24 changes: 12 additions & 12 deletions book/essentials.rst
Original file line number Diff line number Diff line change
Expand Up @@ -17,31 +17,31 @@ The Graph
Authentication
==============

Neo4j 2.2 introduces token-based authentication for database servers. To use a server with
authentication enabled, an auth token must first be obtained and then either supplied to the
:func:`.set_auth_token` function or set as the value of the ``NEO4J_AUTH_TOKEN`` environment
variable.
Neo4j 2.2 introduces optional authentication for database servers, enabled by default.
To use a server with authentication enabled, a user name and password must be specified for the `host:port` combination.
This can either be passed in code using the :func:`.authenticate` function or specified in the ``NEO4J_AUTH`` environment variable.
By default the user name and password are ``neo4j`` and ``neo4j`` respectively.
This default password generally requires an initial change before the database can be used.

There are two ways to set up authentication for a new server installation:

1. Set an initial password for the ``neo4j`` user.
2. Copy auth details from another (initialised) server.

Py2neo provides a command line tool to help with setting the password and retrieving the auth
token. For a new installation, use::
Py2neo provides a command line tool to help with changing user passwords as well as checking whether a password change is required.
For a new installation, use::

$ neoauth neo4j neo4j my-p4ssword
4ff5167fbeedb3082974c3695bc948dc
Password change succeeded

For subsequent usage, after the initial password has been set::
After a password has been set, the tool can also be used to validate credentials::

$ neoauth neo4j my-p4ssword
4ff5167fbeedb3082974c3695bc948dc
Password change not required

Alternatively, authentication can be disabled completely by editing the value of the
``dbms.security.authorization_enabled`` setting in the ``conf/neo4j-server.properties`` file.
Alternatively, authentication can be disabled completely by editing the value of the ``dbms.security.authorization_enabled`` setting in the ``conf/neo4j-server.properties`` file.

.. autofunction:: py2neo.set_auth_token
.. autofunction:: py2neo.authenticate


Nodes
Expand Down
2 changes: 1 addition & 1 deletion py2neo/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@

__all__ = ["Graph", "Node", "Relationship", "Path", "NodePointer", "Rel", "Rev", "Subgraph",
"ServiceRoot", "PropertySet", "LabelSet", "PropertyContainer",
"authenticate", "familiar", "set_auth_token", "rewrite", "watch",
"authenticate", "familiar", "rewrite", "watch",
"BindError", "Finished", "GraphError", "JoinError", "Unauthorized",
"ServerPlugin", "UnmanagedExtension", "Service", "Resource", "ResourceTemplate",
"LegacyNode", "node", "rel",
Expand Down
27 changes: 8 additions & 19 deletions py2neo/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
import webbrowser

from py2neo import __version__
from py2neo.env import NEO4J_AUTH_TOKEN, NEO4J_URI
from py2neo.env import NEO4J_AUTH, NEO4J_URI
from py2neo.error import BindError, GraphError, JoinError, Unauthorized
from py2neo.packages.httpstream import http, ClientError, ServerError, \
Resource as _Resource, ResourceTemplate as _ResourceTemplate
Expand All @@ -39,7 +39,7 @@

__all__ = ["Graph", "Node", "Relationship", "Path", "NodePointer", "Rel", "Rev", "Subgraph",
"ServiceRoot", "PropertySet", "LabelSet", "PropertyContainer",
"authenticate", "familiar", "set_auth_token", "rewrite",
"authenticate", "familiar", "rewrite",
"ServerPlugin", "UnmanagedExtension", "Service", "Resource", "ResourceTemplate"]


Expand Down Expand Up @@ -81,8 +81,9 @@ def _get_headers(host_port):


def authenticate(host_port, user_name, password, realm=None):
""" Set HTTP basic authentication values for specified `host_port`. The
code below shows a simple example::
""" Set HTTP basic authentication values for specified `host_port` for use
with both Neo4j 2.2 built-in authentication as well as if a database server
is behind (for example) an Apache proxy. The code below shows a simple example::
from py2neo import authenticate, Graph
Expand Down Expand Up @@ -128,19 +129,6 @@ def familiar(*objects):
return True


def set_auth_token(host_port, token):
""" Set the auth token for the specified `host_port`. This only applies
to server version 2.2 and above when authentication is turned on. An
alternative to using this function is to supply a token value in the
``NEO4J_AUTH_TOKEN`` environment variable, from where it will be
automatically applied.
:arg host_port: the host and optional port requiring authentication
:arg token: a valid auth token
"""
authenticate(host_port, "", token, "Neo4j")


def rewrite(from_scheme_host_port, to_scheme_host_port):
""" Automatically rewrite all URIs directed to the scheme, host and port
specified in `from_scheme_host_port` to that specified in
Expand Down Expand Up @@ -491,8 +479,9 @@ def __new__(cls, uri=None):
try:
inst = cls.__instances[uri]
except KeyError:
if NEO4J_AUTH_TOKEN:
set_auth_token(URI(uri).host_port, NEO4J_AUTH_TOKEN)
if NEO4J_AUTH:
user_name, password = NEO4J_AUTH.partition(":")[0::2]
authenticate(URI(uri).host_port, user_name, password)
inst = super(ServiceRoot, cls).__new__(cls)
inst.__resource = Resource(uri)
inst.__graph = None
Expand Down
6 changes: 3 additions & 3 deletions py2neo/env.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,11 @@
from py2neo.packages.httpstream.packages.urimagic import URI


__all__ = ["NEO4J_AUTH_TOKEN", "NEO4J_DIST", "NEO4J_HOME", "NEO4J_URI"]
__all__ = ["NEO4J_AUTH", "NEO4J_DIST", "NEO4J_HOME", "NEO4J_URI"]


#: Auth token for use in Neo4j 2.2 and above.
NEO4J_AUTH_TOKEN = os.getenv("NEO4J_AUTH_TOKEN", None)
#: Auth string, stored as `user:password`.
NEO4J_AUTH = os.getenv("NEO4J_AUTH", None)

#: Base URI for downloading Neo4j distribution archives.
NEO4J_DIST = os.getenv("NEO4J_DIST", "http://dist.neo4j.org/")
Expand Down
150 changes: 72 additions & 78 deletions py2neo/password.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,11 @@

from __future__ import print_function

import base64
import os
import sys

from py2neo import GraphError, Resource, Service, ServiceRoot
from py2neo import GraphError, Service, ServiceRoot
from py2neo.env import NEO4J_URI
from py2neo.packages.httpstream.numbers import UNPROCESSABLE_ENTITY
from py2neo.util import ustr
Expand All @@ -31,38 +32,48 @@
Usage: {script} «user_name» «password» [«new_password»]
Authenticate against a Neo4j database server, optionally changing
the password.
the password. If no new password is supplied, a simple auth check
is carried out and a response is output describing whether or not
a password change is required. If the new password is supplied, a
password change is attempted.
Report bugs to nigel@py2neo.org
"""


class AuthenticationError(GraphError):
""" Authentication failed with the supplied credentials.
"""

def auth_header_value(user_name, password, realm=None):
""" Construct a value for the Authorization header based on the
credentials supplied.
class PasswordChangeRequired(GraphError):
""" A new password is required before authentication can occur.
:param user_name: the user name
:param password: the password
:param realm: the realm (optional)
:return: string to be included in Authorization header
"""
credentials = (user_name + ":" + password).encode("UTF-8")
if realm:
value = 'Basic realm="' + realm + '" '
else:
value = 'Basic '
value += base64.b64encode(credentials).decode("ASCII")
return value


class Authentication(Service):
""" Authentication management service.
class UserManager(Service):
""" User management service.
"""

@classmethod
def for_service(cls, service_root):
""" Fetch an Authentication instance for a given service root.
def for_user(cls, service_root, user_name, password):
""" Fetch a UserManager instance for a given service root and user name.
:param service_root: A valid :class:`py2neo.ServiceRoot` instance.
:rtype: :class:`.Authentication`
:rtype: :class:`.UserManager`
"""
try:
return cls(service_root.resource.metadata["authentication"])
except KeyError:
raise NotImplementedError("Authentication is not required for the service "
"at %r" % service_root.uri.string)
uri = service_root.uri.resolve("/user/%s" % user_name)
inst = cls(uri)
inst.resource.headers["Authorization"] = auth_header_value(user_name, password, "Neo4j")
return inst

__instances = {}

Expand All @@ -73,47 +84,33 @@ def __new__(cls, uri):
try:
inst = cls.__instances[uri]
except KeyError:
inst = super(Authentication, cls).__new__(cls)
inst = super(UserManager, cls).__new__(cls)
inst.bind(uri)
cls.__instances[uri] = inst
return inst

def refresh(self, user_name, password):
""" Perform authentication to refresh the stored metadata.
:arg user_name: Name of user to authenticate as.
:arg password: The current password for this user.
def refresh(self):
""" Refresh the stored metadata.
"""
try:
self.metadata = self.resource.post({"username": user_name,
"password": password}).content
except GraphError as error:
if error.response.status_code == UNPROCESSABLE_ENTITY:
raise AuthenticationError("Cannot authenticate for user %r" % user_name)
else:
raise
rs = self.resource.get()
self.metadata = rs.content

def authenticate(self, user_name, password, new_password=None):
""" Authenticate to retrieve an auth token.
@property
def user_name(self):
self.refresh()
return self.metadata["username"]

:arg service_root: The :class:`py2neo.ServiceRoot` object requiring authentication.
:arg user_name: Name of user to authenticate as.
:arg password: The current password for this user.
:arg new_password: A new password (optional, must not match the current password).
:return: A valid auth token.
:raise ValueError: If the new password supplied is invalid.
:raise AuthenticationError: If the user cannot be authenticated.
"""
self.refresh(user_name, password)
if new_password is None:
if self.metadata["password_change_required"]:
raise PasswordChangeRequired("A password change is required for the service "
"at %r" % self.service_root.uri.string)
else:
return self.metadata.get("authorization_token")
else:
password_manager = PasswordManager(self.metadata["password_change"])
return password_manager.change(password, new_password)
@property
def password_manager(self):
self.refresh()
password_manager = PasswordManager(self.metadata["password_change"])
password_manager.resource.headers["Authorization"] = self.resource.headers["Authorization"]
return password_manager

@property
def password_change_required(self):
self.refresh()
return self.metadata["password_change_required"]


class PasswordManager(Service):
Expand All @@ -131,7 +128,7 @@ def __new__(cls, uri):
cls.__instances[uri] = inst
return inst

def change(self, password, new_password):
def change(self, new_password):
""" Change the authentication password.
:arg password: The current password.
Expand All @@ -140,30 +137,14 @@ def change(self, password, new_password):
:raise ValueError: If the new password is invalid.
"""
try:
response = self.resource.post({"password": password, "new_password": new_password})
response = self.resource.post({"password": new_password})
except GraphError as error:
if error.response.status_code == UNPROCESSABLE_ENTITY:
raise ValueError("Cannot change password")
else:
raise
else:
return response.content.get("authorization_token")


def get_auth_token(uri, user_name, password, new_password=None):
""" Authenticate to retrieve an auth token.
:arg uri: The root URI for the service requiring authentication.
:arg user_name: Name of user to authenticate as.
:arg password: The current password for this user.
:arg new_password: A new password (optional, must not match the current password).
:return: A valid auth token.
:raise AuthenticationError: If the user cannot be authenticated.
:raise ValueError: If the new password value is invalid.
"""
auth = Authentication.for_service(ServiceRoot(uri))
return auth.authenticate(user_name, password, new_password)
return response.status_code == 200


def _help(script):
Expand All @@ -172,17 +153,30 @@ def _help(script):

def main():
script, args = sys.argv[0], sys.argv[1:]
if len(args) < 2 or len(args) > 3:
_help(script)
return
try:
if args:
if 2 <= len(args) <= 3:
print(get_auth_token(NEO4J_URI, *args))
service_root = ServiceRoot(NEO4J_URI)
user_name = args[0]
password = args[1]
user_manager = UserManager.for_user(service_root, user_name, password)
if len(args) == 2:
# Check password
if user_manager.password_change_required:
print("Password change required")
else:
_help(script)
print("Password change not required")
else:
_help(script)
# Change password
password_manager = user_manager.password_manager
new_password = args[2]
if password_manager.change(new_password):
print("Password change succeeded")
else:
print("Password change failed")
except Exception as error:
sys.stderr.write(ustr(error))
sys.stderr.write("\n")
sys.stderr.write("%s: %s\n" % (error.__class__.__name__, ustr(error)))
sys.exit(1)


Expand Down

0 comments on commit 3da8610

Please sign in to comment.