diff --git a/Makefile b/Makefile index 4decb77..fc09af5 100644 --- a/Makefile +++ b/Makefile @@ -7,7 +7,8 @@ define PROJECT_ENV {http_method, get}, {user_path, "http://localhost:8000/auth/user"}, {vhost_path, "http://localhost:8000/auth/vhost"}, - {resource_path, "http://localhost:8000/auth/resource"} + {resource_path, "http://localhost:8000/auth/resource"}, + {topic_path, "http://localhost:8000/auth/topic"} ] endef diff --git a/README.md b/README.md index 1e0990b..c540cc5 100644 --- a/README.md +++ b/README.md @@ -56,6 +56,7 @@ In `rabbitmq.conf` (currently RabbitMQ master): rabbitmq_auth_backend_http.user_path = http://some-server/auth/user rabbitmq_auth_backend_http.vhost_path = http://some-server/auth/vhost rabbitmq_auth_backend_http.resource_path = http://some-server/auth/resource + rabbitmq_auth_backend_http.topic_path = http://some-server/auth/topic In the classic config format (`rabbitmq.config` prior to 3.7.0 or `advanced.config`): @@ -65,7 +66,8 @@ In the classic config format (`rabbitmq.config` prior to 3.7.0 or `advanced.conf [{http_method, post}, {user_path, "http(s)://some-server/auth/user"}, {vhost_path, "http(s)://some-server/auth/vhost"}, - {resource_path, "http(s)://some-server/auth/resource"}]} + {resource_path, "http(s)://some-server/auth/resource"}, + {topic_path, "http(s)://some-server/auth/topic"}]} ]. By default `http_method` configuration is `GET` for backwards compatibility. It's recommended @@ -93,11 +95,23 @@ Note that you cannot create arbitrary virtual hosts using this plugin; you can o ### resource_path -* `username` - the name of the user -* `vhost` - the name of the virtual host containing the resource -* `resource` - the type of resource (`exchange`, `queue`) -* `name` - the name of the resource -* `permission` - the access level to the resource (`configure`, `write`, `read`) - see [the Access Control guide](http://www.rabbitmq.com/access-control.html) for their meaning +* `username` - the name of the user +* `vhost` - the name of the virtual host containing the resource +* `resource` - the type of resource (`exchange`, `queue`, `topic`) +* `name` - the name of the resource +* `permission` - the access level to the resource (`configure`, `write`, `read`) - see [the Access Control guide](http://www.rabbitmq.com/access-control.html) for their meaning + +### topic_path + +* `username` - the name of the user +* `vhost` - the name of the virtual host containing the resource +* `resource` - the type of resource (`topic` in this case) +* `name` - the name of the exchange +* `permission` - the access level to the resource (`write` in this case) +* `routing_key` - the routing key of the published message + +See [topic authorisation](http://www.rabbitmq.com/access-control.html#topic-authorisation) for more information +about topic authorisation. Your web server should always return HTTP 200 OK, with a body containing: @@ -118,6 +132,7 @@ configure the plugin to use a CA and client certificate/key pair using the `rabb {user_path, "https://some-server/auth/user"}, {vhost_path, "https://some-server/auth/vhost"}, {resource_path, "https://some-server/auth/resource"}, + {topic_path, "https://some-server/auth/topic"}, {ssl_options, [{cacertfile, "/path/to/cacert.pem"}, {certfile, "/path/to/client/cert.pem"}, diff --git a/examples/README b/examples/README deleted file mode 100644 index 00ceae1..0000000 --- a/examples/README +++ /dev/null @@ -1,14 +0,0 @@ -rabbitmq_auth_backend_django is a very very simple Django application -that rabbitmq-auth-backend-http can authenticate against. It's really -not designed to be anything other than an example. - -Run start.sh to launch it after installing Django somehow (e.g. sudo -apt-get install python-django). You may need to hack start.sh if you -are not running Debian / Ubuntu. - -The app will create a SQLite database in /tmp/. It uses the standard -Django authentication database. All users get access to all vhosts and -resources. - -If you're not familiar with Django, urls.py and auth/views.py may be -most illuminating. diff --git a/examples/README.md b/examples/README.md new file mode 100644 index 0000000..e51b709 --- /dev/null +++ b/examples/README.md @@ -0,0 +1,19 @@ +# RabbitMQ HTTP Authn/Authz Backend Example + +`rabbitmq_auth_backend_django` is a very minimalistic [Django](https://www.djangoproject.com/) 1.10+ application +that rabbitmq-auth-backend-http can authenticate against. It's really +not designed to be anything other than an example. + +## Running the Example + +Run `start.sh` to launch it after [installing Django](https://docs.djangoproject.com/en/1.10/topics/install/). You may need to +hack `start.sh` if you are not running Debian or Ubuntu. + +The app will use a local SQLite database. It uses the standard +Django authentication database. All users get access to all vhosts and +resources. + +## HTTP Endpoint Examples + +If you're not familiar with Django, urls.py and auth/views.py may be +most illuminating. diff --git a/examples/rabbitmq_auth_backend_django/.gitignore b/examples/rabbitmq_auth_backend_django/.gitignore new file mode 100644 index 0000000..6e9bc0c --- /dev/null +++ b/examples/rabbitmq_auth_backend_django/.gitignore @@ -0,0 +1 @@ +*.sqlite3 \ No newline at end of file diff --git a/examples/rabbitmq_auth_backend_django/manage.py b/examples/rabbitmq_auth_backend_django/manage.py old mode 100644 new mode 100755 index b51a255..1ae2e80 --- a/examples/rabbitmq_auth_backend_django/manage.py +++ b/examples/rabbitmq_auth_backend_django/manage.py @@ -4,7 +4,19 @@ if __name__ == "__main__": os.environ.setdefault("DJANGO_SETTINGS_MODULE", "rabbitmq_auth_backend_django.settings") - - from django.core.management import execute_from_command_line - + try: + from django.core.management import execute_from_command_line + except ImportError: + # The above import may fail for some other reason. Ensure that the + # issue is really that Django is missing to avoid masking other + # exceptions on Python 2. + try: + import django + except ImportError: + raise ImportError( + "Couldn't import Django. Are you sure it's installed and " + "available on your PYTHONPATH environment variable? Did you " + "forget to activate a virtual environment?" + ) + raise execute_from_command_line(sys.argv) diff --git a/examples/rabbitmq_auth_backend_django/rabbitmq_auth_backend_django/auth/views.py b/examples/rabbitmq_auth_backend_django/rabbitmq_auth_backend_django/auth/views.py index a2de5c7..a2a8ff4 100644 --- a/examples/rabbitmq_auth_backend_django/rabbitmq_auth_backend_django/auth/views.py +++ b/examples/rabbitmq_auth_backend_django/rabbitmq_auth_backend_django/auth/views.py @@ -18,3 +18,6 @@ def vhost(request): def resource(request): return HttpResponse("allow") + +def topic(request): + return HttpResponse("allow") diff --git a/examples/rabbitmq_auth_backend_django/rabbitmq_auth_backend_django/settings.py b/examples/rabbitmq_auth_backend_django/rabbitmq_auth_backend_django/settings.py index 7da6cb0..0e716a0 100644 --- a/examples/rabbitmq_auth_backend_django/rabbitmq_auth_backend_django/settings.py +++ b/examples/rabbitmq_auth_backend_django/rabbitmq_auth_backend_django/settings.py @@ -1,59 +1,77 @@ """ Django settings for rabbitmq_auth_backend_django project. +Generated by 'django-admin startproject' using Django 1.10.5. + For more information on this file, see -https://docs.djangoproject.com/en/1.6/topics/settings/ +https://docs.djangoproject.com/en/1.10/topics/settings/ For the full list of settings and their values, see -https://docs.djangoproject.com/en/1.6/ref/settings/ +https://docs.djangoproject.com/en/1.10/ref/settings/ """ -# Build paths inside the project like this: os.path.join(BASE_DIR, ...) import os -BASE_DIR = os.path.dirname(os.path.dirname(__file__)) + +# Build paths inside the project like this: os.path.join(BASE_DIR, ...) +BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) # Quick-start development settings - unsuitable for production -# See https://docs.djangoproject.com/en/1.6/howto/deployment/checklist/ +# See https://docs.djangoproject.com/en/1.10/howto/deployment/checklist/ # SECURITY WARNING: keep the secret key used in production secret! -SECRET_KEY = 'fv_vu$nf)h&lltdd)y=)x)^f23)k0s#01yo@^$06w4e29f813e' +SECRET_KEY = '_wqlwxs-s(na_@1-@3=6uc2=-ka3f)))%-v#lgx4een8^#u92c' # SECURITY WARNING: don't run with debug turned on in production! DEBUG = True -TEMPLATE_DEBUG = True - ALLOWED_HOSTS = [] # Application definition -INSTALLED_APPS = ( +INSTALLED_APPS = [ 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', -) +] -MIDDLEWARE_CLASSES = ( +MIDDLEWARE = [ + 'django.middleware.security.SecurityMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware', 'django.middleware.common.CommonMiddleware', 'django.middleware.csrf.CsrfViewMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware', 'django.contrib.messages.middleware.MessageMiddleware', 'django.middleware.clickjacking.XFrameOptionsMiddleware', -) +] ROOT_URLCONF = 'rabbitmq_auth_backend_django.urls' +TEMPLATES = [ + { + 'BACKEND': 'django.template.backends.django.DjangoTemplates', + 'DIRS': [], + 'APP_DIRS': True, + 'OPTIONS': { + 'context_processors': [ + 'django.template.context_processors.debug', + 'django.template.context_processors.request', + 'django.contrib.auth.context_processors.auth', + 'django.contrib.messages.context_processors.messages', + ], + }, + }, +] + WSGI_APPLICATION = 'rabbitmq_auth_backend_django.wsgi.application' # Database -# https://docs.djangoproject.com/en/1.6/ref/settings/#databases +# https://docs.djangoproject.com/en/1.10/ref/settings/#databases DATABASES = { 'default': { @@ -62,8 +80,28 @@ } } + +# Password validation +# https://docs.djangoproject.com/en/1.10/ref/settings/#auth-password-validators + +AUTH_PASSWORD_VALIDATORS = [ + { + 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', + }, +] + + # Internationalization -# https://docs.djangoproject.com/en/1.6/topics/i18n/ +# https://docs.djangoproject.com/en/1.10/topics/i18n/ LANGUAGE_CODE = 'en-us' @@ -77,6 +115,6 @@ # Static files (CSS, JavaScript, Images) -# https://docs.djangoproject.com/en/1.6/howto/static-files/ +# https://docs.djangoproject.com/en/1.10/howto/static-files/ STATIC_URL = '/static/' diff --git a/examples/rabbitmq_auth_backend_django/rabbitmq_auth_backend_django/urls.py b/examples/rabbitmq_auth_backend_django/rabbitmq_auth_backend_django/urls.py index 9fce0fa..671d4e0 100644 --- a/examples/rabbitmq_auth_backend_django/rabbitmq_auth_backend_django/urls.py +++ b/examples/rabbitmq_auth_backend_django/rabbitmq_auth_backend_django/urls.py @@ -1,14 +1,26 @@ -from django.conf.urls import patterns, include, url +"""rabbitmq_auth_backend_django URL Configuration -# Uncomment the next two lines to enable the admin: +The `urlpatterns` list routes URLs to views. For more information please see: + https://docs.djangoproject.com/en/1.10/topics/http/urls/ +Examples: +Function views + 1. Add an import: from my_app import views + 2. Add a URL to urlpatterns: url(r'^$', views.home, name='home') +Class-based views + 1. Add an import: from other_app.views import Home + 2. Add a URL to urlpatterns: url(r'^$', Home.as_view(), name='home') +Including another URLconf + 1. Import the include() function: from django.conf.urls import url, include + 2. Add a URL to urlpatterns: url(r'^blog/', include('blog.urls')) +""" +from django.conf.urls import url from django.contrib import admin -admin.autodiscover() +import rabbitmq_auth_backend_django.auth.views as views -urlpatterns = patterns('', - # Example: - (r'^auth/user', 'rabbitmq_auth_backend_django.auth.views.user'), - (r'^auth/vhost', 'rabbitmq_auth_backend_django.auth.views.vhost'), - (r'^auth/resource', 'rabbitmq_auth_backend_django.auth.views.resource'), - (r'^admin/doc/', include('django.contrib.admindocs.urls')), - (r'^admin/', include(admin.site.urls)), -) +urlpatterns = [ + url(r'^auth/user', views.user), + url(r'^auth/vhost', views.vhost), + url(r'^auth/resource', views.resource), + url(r'^auth/topic', views.topic), + url(r'^admin/', admin.site.urls), +] diff --git a/examples/rabbitmq_auth_backend_django/rabbitmq_auth_backend_django/wsgi.py b/examples/rabbitmq_auth_backend_django/rabbitmq_auth_backend_django/wsgi.py index 189e1a3..70a321f 100644 --- a/examples/rabbitmq_auth_backend_django/rabbitmq_auth_backend_django/wsgi.py +++ b/examples/rabbitmq_auth_backend_django/rabbitmq_auth_backend_django/wsgi.py @@ -4,11 +4,13 @@ It exposes the WSGI callable as a module-level variable named ``application``. For more information on this file, see -https://docs.djangoproject.com/en/1.6/howto/deployment/wsgi/ +https://docs.djangoproject.com/en/1.10/howto/deployment/wsgi/ """ import os -os.environ.setdefault("DJANGO_SETTINGS_MODULE", "rabbitmq_auth_backend_django.settings") from django.core.wsgi import get_wsgi_application + +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "rabbitmq_auth_backend_django.settings") + application = get_wsgi_application() diff --git a/examples/rabbitmq_auth_backend_django/start.sh b/examples/rabbitmq_auth_backend_django/start.sh index 857060d..4d9900d 100755 --- a/examples/rabbitmq_auth_backend_django/start.sh +++ b/examples/rabbitmq_auth_backend_django/start.sh @@ -1,3 +1,3 @@ #!/bin/sh -python manage.py syncdb +python manage.py migrate python manage.py runserver diff --git a/priv/schema/rabbitmq_auth_backend_http.schema b/priv/schema/rabbitmq_auth_backend_http.schema index f10eb67..30e37bc 100644 --- a/priv/schema/rabbitmq_auth_backend_http.schema +++ b/priv/schema/rabbitmq_auth_backend_http.schema @@ -13,3 +13,6 @@ {mapping, "rabbitmq_auth_backend_http.resource_path", "rabbitmq_auth_backend_http.resource_path", [{datatype, string}, {validators, ["uri"]}]}. + +{mapping, "rabbitmq_auth_backend_http.topic_path", "rabbitmq_auth_backend_http.topic_path", + [{datatype, string}, {validators, ["uri"]}]}. diff --git a/src/rabbit_auth_backend_http.erl b/src/rabbit_auth_backend_http.erl index 3202801..32e747f 100644 --- a/src/rabbit_auth_backend_http.erl +++ b/src/rabbit_auth_backend_http.erl @@ -23,11 +23,13 @@ -export([description/0, p/1, q/1]). -export([user_login_authentication/2, user_login_authorization/1, - check_vhost_access/3, check_resource_access/3]). + check_vhost_access/3, check_resource_access/3, check_topic_access/4]). %% If keepalive connection is closed, retry N times before failing. -define(RETRY_ON_KEEPALIVE_CLOSED, 3). +-define(RESOURCE_REQUEST_PARAMETERS, [username, vhost, resource, name, permission]). + %%-------------------------------------------------------------------- description() -> @@ -68,8 +70,29 @@ check_resource_access(#auth_user{username = Username}, {name, Name}, {permission, Permission}]). +check_topic_access(#auth_user{username = Username}, + #resource{virtual_host = VHost, kind = topic = Type, name = Name}, + Permission, + Context) -> + OptionsParameters = context_as_parameters(Context), + bool_req(topic_path, [{username, Username}, + {vhost, VHost}, + {resource, Type}, + {name, Name}, + {permission, Permission}] ++ OptionsParameters). + %%-------------------------------------------------------------------- +context_as_parameters(Options) when is_map(Options) -> + % filter keys that would erase fixed parameters + [{rabbit_data_coercion:to_atom(Key), maps:get(Key, Options)} + || Key <- maps:keys(Options), + lists:member( + rabbit_data_coercion:to_atom(Key), + ?RESOURCE_REQUEST_PARAMETERS) =:= false]; +context_as_parameters(_) -> + []. + bool_req(PathName, Props) -> case http_req(p(PathName), q(Props)) of "deny" -> false;