Skip to content

Commit

Permalink
Merge pull request #139 from neo4jrb/auth
Browse files Browse the repository at this point in the history
Neo4j authentication
  • Loading branch information
subvertallchris committed Dec 5, 2014
2 parents ccf2642 + 1969f25 commit b964403
Show file tree
Hide file tree
Showing 13 changed files with 433 additions and 94 deletions.
4 changes: 2 additions & 2 deletions .travis.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
script: "bundle exec rake neo4j:install['community-2.1.4'] neo4j:start default --trace"
script: "bundle exec rake neo4j:install['community-2.2.0-M01'] neo4j:disable_auth neo4j:start default --trace"
language: ruby
rvm:
- 2.1.1
- 2.1.5
- jruby-19mode
1 change: 1 addition & 0 deletions lib/neo4j-server.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
require 'neo4j-server/resource'
require 'neo4j-server/cypher_node'
require 'neo4j-server/cypher_label'
require 'neo4j-server/cypher_authentication'
require 'neo4j-server/cypher_session'
require 'neo4j-server/cypher_node_uncommited'
require 'neo4j-server/cypher_relationship'
Expand Down
101 changes: 101 additions & 0 deletions lib/neo4j-server/cypher_authentication.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
module Neo4j::Server
# Neo4j 2.2 has an authentication layer. This class provides methods for interacting with it.
class CypherAuthentication
class InvalidPasswordError < RuntimeError; end
class PasswordChangeRequiredError < RuntimeError; end
class MissingCredentialsError < RuntimeError; end

attr_reader :connection, :url, :params, :token

# @param [String] url_string The server address with protocol and port.
# @param [Faraday::Connection] session_connection A Faraday::Connection object. This is either an existing object, likely the
# same object used by the server for data, or a new one created specifically for auth tasks.
# @param [Hash] params_hash Faraday connection options. In particularly, we're looking for basic_auth creds.
def initialize(url_string, session_connection = new_connection, params_hash = {})
@url = url_string
@connection = session_connection
@params = params_hash
end

# Set the username and password used to communicate with the server.
def basic_auth(username, password)
params[:basic_auth] ||= {}
params[:basic_auth][:username] = username
params[:basic_auth][:password] = password
end

# POSTs to the password change endpoint of the API. Does not invalidate tokens.
# @param [String] old_password The current password.
# @param [String] new_password The password you want to use.
# @return [Hash] The response from the server.
def change_password(old_password, new_password)
connection.post("#{url}/user/neo4j/password", { 'password' => old_password, 'new_password' => new_password }).body
end

# Uses the given username and password to obtain a token, then adds the token to the connection's parameters.
# @return [String] An access token provided by the server.
def authenticate
auth_response = connection.get("#{url}/authentication")
return nil if auth_response.body.empty?
auth_body = JSON.parse(auth_response.body)
token = auth_body['errors'][0]['code'] == 'Neo.ClientError.Security.AuthorizationFailed' ? obtain_token : nil
add_auth_headers(token) unless token.nil?
end

# Invalidates the existing token, which will invalidate all conncetions using this token, applies for a new token, adds this into
# the connection headers.
# @param [String] password The current server password.
def reauthenticate(password)
invalidate_token(password)
add_auth_headers(obtain_token)
end

# Requests a token from the authentication endpoint using the given username and password.
# @return [String] A plain-text token.
def obtain_token
begin
user = params[:basic_auth][:username]
pass = params[:basic_auth][:password]
rescue NoMethodError
raise MissingCredentialsError, 'Neo4j authentication is enabled, username/password are required but missing'
end
auth_response = connection.post("#{url}/authentication", { 'username' => user, 'password' => pass })
raise PasswordChangeRequiredError, "Server requires a password change, please visit #{url}" if auth_response.body['password_change_required']
raise InvalidPasswordError, "Neo4j server responded with: #{auth_response.body['errors'][0]['message']}" if auth_response.status.to_i == 422
auth_response.body['authorization_token']
end

# Invalidates tokens as described at http://neo4j.com/docs/snapshot/rest-api-security.html#rest-api-invalidating-the-authorization-token
# @param [String] current_password The current password used to connect to the database
def invalidate_token(current_password)
connection.post("#{url}/user/neo4j/authorization_token", { 'password' => current_password }).body
end

# Stores an authentication token in the properly-formatted header.
# @param [String] token The authentication token provided by the database.
def add_auth_headers(token)
@token = token
connection.headers['Authorization'] = "Basic realm=\"Neo4j\" #{token_hash(token)}"
end

private

def self.new_connection
conn = Faraday.new do |b|
b.request :json
b.response :json, :content_type => "application/json"
b.use Faraday::Adapter::NetHttpPersistent
end
conn.headers = { 'Content-Type' => 'application/json' }
conn
end

def new_connection
self.class.new_connection
end

def token_hash(token)
Base64.strict_encode64(":#{token}")
end
end
end
31 changes: 18 additions & 13 deletions lib/neo4j-server/cypher_session.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,15 @@ class CypherSession < Neo4j::Session
include Neo4j::Core::CypherTranslator

alias_method :super_query, :query
attr_reader :connection
attr_reader :connection, :auth

def initialize(data_url, connection, auth_obj = nil)
@connection = connection
@auth = auth_obj if auth_obj
Neo4j::Session.register(self)
initialize_resource(data_url)
Neo4j::Session._notify_listeners(:session_available, self)
end

# @param [Hash] params could be empty or contain basic authentication user and password
# @return [Faraday]
Expand All @@ -26,7 +34,7 @@ def self.create_connection(params)
b.use Faraday::Adapter::NetHttpPersistent
# b.adapter Faraday.default_adapter
end
conn.headers = {'Content-Type' => 'application/json', 'User-Agent' => ::Neo4j::Session.user_agent_string}
conn.headers = { 'Content-Type' => 'application/json', 'User-Agent' => ::Neo4j::Session.user_agent_string }
conn
end

Expand All @@ -35,18 +43,21 @@ def self.create_connection(params)
#
# @param [String] endpoint_url - the url to the neo4j server, defaults to 'http://localhost:7474'
# @param [Hash] params faraday params, see #create_connection or an already created faraday connection
def self.open(endpoint_url=nil, params = {})
def self.open(endpoint_url = nil, params = {})
extract_basic_auth(endpoint_url, params)
connection = params[:connection] || create_connection(params)
url = endpoint_url || 'http://localhost:7474'
auth_obj = CypherAuthentication.new(url, connection, params)
auth_obj.authenticate
response = connection.get(url)
raise "Server not available on #{url} (response code #{response.status})" unless response.status == 200
establish_session(response.body, connection, auth_obj)
end

root_data = response.body
def self.establish_session(root_data, connection, auth_obj)
data_url = root_data['data']
data_url << '/' unless data_url.end_with?('/')

CypherSession.new(data_url, connection)
CypherSession.new(data_url, connection, auth_obj)
end

def self.extract_basic_auth(url, params)
Expand All @@ -56,14 +67,8 @@ def self.extract_basic_auth(url, params)
password: URI(url).password
}
end
private_class_method :extract_basic_auth

def initialize(data_url, connection)
@connection = connection
Neo4j::Session.register(self)
initialize_resource(data_url)
Neo4j::Session._notify_listeners(:session_available, self)
end
private_class_method :extract_basic_auth

def db_type
:server_db
Expand Down
26 changes: 21 additions & 5 deletions lib/neo4j/tasks/config_server.rb
Original file line number Diff line number Diff line change
@@ -1,15 +1,31 @@
module Neo4j
module Tasks

module ConfigServer

def config(data, port)
s = set_property(data, 'org.neo4j.server.webserver.https.enabled', 'false')
def config(source_text, port)
s = set_property(source_text, 'org.neo4j.server.webserver.https.enabled', 'false')
set_property(s, 'org.neo4j.server.webserver.port', port)
end

def set_property(data, property, value)
data.gsub(/#{property}\s*=\s*(\w+)/, "#{property}=#{value}")
def set_property(source_text, property, value)
source_text.gsub(/#{property}\s*=\s*(\w+)/, "#{property}=#{value}")
end

# Toggles the status of Neo4j 2.2's basic auth
def toggle_auth(status, source_text)
status_string = status == :enable ? 'true' : 'false'
set_property(source_text, 'dbms.security.authorization_enabled', status_string)
end

# POSTs to an endpoint with the form required to change a Neo4j password
# @param [String] target_address The server address, with protocol and port, against which the form should be POSTed
# @param [String] old_password The existing password for the "neo4j" user account
# @param [String] new_password The new password you want to use. Shocking, isn't it?
# @return [Hash] The response from the server indicating success/failure.
def change_password(target_address, old_password, new_password)
uri = URI.parse("#{target_address}/user/neo4j/password")
response = Net::HTTP.post_form(uri, { 'password' => old_password, 'new_password' => new_password })
JSON.parse(response.body)
end

extend self
Expand Down

0 comments on commit b964403

Please sign in to comment.