commit c8a38c0e41e8a31ae4ca793dd5b4cffdb2225ff3
Author: Christos Stavrakakis <cstavr@grnet.gr>
Date:   Thu Nov 21 14:09:15 2013 +0200

    cyclades: Export Astakos detailed stats from API
    
    Add new 'astakos_admin' service to export basic statistics about Astakos
    service. The stats are exported via the '/admin/stats/detail' API endpoint.
    Access to this view is only allowed to the users that belong to specific
    Astakos groups, as specified in the new setting
    'ASTAKOS_ADMIN_STATS_PERMITTED_GROUPS.'

diff --git a/snf-astakos-app/astakos/admin/__init__.py b/snf-astakos-app/astakos/admin/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/snf-astakos-app/astakos/admin/admin_urls.py b/snf-astakos-app/astakos/admin/admin_urls.py
new file mode 100644
index 0000000..f18ce76
--- /dev/null
+++ b/snf-astakos-app/astakos/admin/admin_urls.py
@@ -0,0 +1,49 @@
+# Copyright 2013 GRNET S.A. All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or
+# without modification, are permitted provided that the following
+# conditions are met:
+#
+#   1. Redistributions of source code must retain the above
+#      copyright notice, this list of conditions and the following
+#      disclaimer.
+#
+#   2. Redistributions in binary form must reproduce the above
+#      copyright notice, this list of conditions and the following
+#      disclaimer in the documentation and/or other materials
+#      provided with the distribution.
+#
+# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS
+# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR
+# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
+# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
+# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
+# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+# POSSIBILITY OF SUCH DAMAGE.
+#
+# The views and conclusions contained in the software and
+# documentation are those of the authors and should not be
+# interpreted as representing official policies, either expressed
+# or implied, of GRNET S.A.
+
+from django.conf.urls import url, patterns
+
+from astakos.admin import views
+from django.http import Http404
+
+
+def index(request):
+    raise Http404
+
+
+urlpatterns = patterns(
+    '',
+    url(r'^$', index),
+    url(r'^stats$', views.get_public_stats),
+    url(r'^stats/detail$', views.get_astakos_stats),
+)
diff --git a/snf-astakos-app/astakos/admin/stats.py b/snf-astakos-app/astakos/admin/stats.py
new file mode 100644
index 0000000..cf0cdbc
--- /dev/null
+++ b/snf-astakos-app/astakos/admin/stats.py
@@ -0,0 +1,88 @@
+# Copyright 2013 GRNET S.A. All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or
+# without modification, are permitted provided that the following
+# conditions are met:
+#
+#   1. Redistributions of source code must retain the above
+#      copyright notice, this list of conditions and the following
+#      disclaimer.
+#
+#   2. Redistributions in binary form must reproduce the above
+#      copyright notice, this list of conditions and the following
+#      disclaimer in the documentation and/or other materials
+#      provided with the distribution.
+#
+# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS
+# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR
+# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
+# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
+# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
+# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+# POSSIBILITY OF SUCH DAMAGE.
+#
+# The views and conclusions contained in the software and
+# documentation are those of the authors and should not be
+# interpreted as representing official policies, either expressed
+# or implied, of GRNET S.A.
+import datetime
+from django.conf import settings
+from django.db.models import Sum
+
+from astakos.im.models import AstakosUser, Resource
+from astakos.quotaholder_app.models import Holding
+
+
+def get_public_stats():
+    users = AstakosUser.objects.all()
+    active = users.filter(is_active=True)
+    return {"users": {"total": users.count(),
+                      "active": active.count()}}
+
+
+def get_astakos_stats():
+    stats = {"datetime": datetime.datetime.now().strftime("%c")}
+
+    resources = Resource.objects.values_list("name", flat=True)
+
+    users = AstakosUser.objects.all()
+    verified = users.filter(email_verified=True)
+    active = users.filter(is_active=True)
+
+    user_stats = {}
+    user_stats["total"] = {"total": users.count(),
+                           "verified": verified.count(),
+                           "active": active.count(),
+                           "usage": {}}
+
+    for resource in resources:
+        usage = Holding.objects.filter(resource=resource)\
+                               .aggregate(summ=Sum("usage_max"))
+        user_stats["total"]["usage"][resource] = int(usage["summ"])
+
+    for provider in settings.ASTAKOS_IM_MODULES:
+
+        users = AstakosUser.objects.filter(auth_providers__module=provider)
+        verified = users.filter(email_verified=True)
+        active = users.filter(is_active=True)
+
+        user_stats[provider] = {"total": users.count(),
+                                "verified": verified.count(),
+                                "active": active.count(),
+                                "usage": {}}
+
+        users_uuids = users.values_list("uuid", flat=True)
+        for resource in resources:
+            usage = Holding.objects\
+                           .filter(holder__in=users_uuids, resource=resource)\
+                           .aggregate(summ=Sum("usage_max"))
+            user_stats[provider]["usage"][resource] = int(usage["summ"])
+
+    stats["users"] = user_stats
+
+    return stats
diff --git a/snf-astakos-app/astakos/admin/views.py b/snf-astakos-app/astakos/admin/views.py
new file mode 100644
index 0000000..b06489a
--- /dev/null
+++ b/snf-astakos-app/astakos/admin/views.py
@@ -0,0 +1,63 @@
+# Copyright 2013 GRNET S.A. All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or
+# without modification, are permitted provided that the following
+# conditions are met:
+#
+#   1. Redistributions of source code must retain the above
+#      copyright notice, this list of conditions and the following
+#      disclaimer.
+#
+#   2. Redistributions in binary form must reproduce the above
+#      copyright notice, this list of conditions and the following
+#      disclaimer in the documentation and/or other materials
+#      provided with the distribution.
+#
+# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS
+# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR
+# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
+# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
+# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
+# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+# POSSIBILITY OF SUCH DAMAGE.
+#
+# The views and conclusions contained in the software and
+# documentation are those of the authors and should not be
+# interpreted as representing official policies, either expressed
+# or implied, of GRNET S.A.
+
+import logging
+from django import http
+from django.utils import simplejson as json
+from snf_django.lib import api
+from astakos.im import settings
+
+from astakos.admin import stats
+
+logger = logging.getLogger(__name__)
+
+PERMITTED_GROUPS = settings.ADMIN_STATS_PERMITTED_GROUPS
+
+
+@api.api_method(http_method='GET', user_required=False, token_required=False,
+                logger=logger, serializations=['json'])
+@api.allow_jsonp()
+def get_public_stats(request):
+    _stats = stats.get_public_stats()
+    data = json.dumps(_stats)
+    return http.HttpResponse(data, status=200, content_type='application/json')
+
+
+@api.api_method(http_method='GET', user_required=True, token_required=True,
+                logger=logger, serializations=['json'])
+@api.user_in_groups(permitted_groups=PERMITTED_GROUPS,
+                    logger=logger)
+def get_astakos_stats(request):
+    _stats = stats.get_astakos_stats()
+    data = json.dumps(_stats)
+    return http.HttpResponse(data, status=200, content_type='application/json')
diff --git a/snf-astakos-app/astakos/api/services.py b/snf-astakos-app/astakos/api/services.py
index 98fa6b1..159d971 100644
--- a/snf-astakos-app/astakos/api/services.py
+++ b/snf-astakos-app/astakos/api/services.py
@@ -85,4 +85,15 @@ astakos_services = {
              'publicURL': None},
         ],
     },
+
+    'astakos_admin': {
+        'type': 'astakos_admin',
+        'component': 'astakos',
+        'prefix': 'admin',
+        'public': False,
+        'endpoints': [
+            {'versionId': '',
+             'publicURL': None},
+        ],
+    },
 }
diff --git a/snf-astakos-app/astakos/im/management/commands/stats-astakos.py b/snf-astakos-app/astakos/im/management/commands/stats-astakos.py
index 3eae8ba..e42ed17 100644
--- a/snf-astakos-app/astakos/im/management/commands/stats-astakos.py
+++ b/snf-astakos-app/astakos/im/management/commands/stats-astakos.py
@@ -30,21 +30,15 @@
 # documentation are those of the authors and should not be
 # interpreted as representing official policies, either expressed
 # or implied, of GRNET S.A.
-
-
-import datetime
 import json
 import string
-
 #from optparse import make_option
 
-from django.conf import settings
-from snf_django.management.utils import pprint_table
-
 from snf_django.management.commands import SynnefoCommand, CommandError
-from astakos.im.models import AstakosUser, Resource
-from astakos.quotaholder_app.models import Holding
-from django.db.models import Sum
+from snf_django.management.utils import pprint_table
+#from astakos.im.models import AstakosUser, Resource
+#from astakos.quotaholder_app.models import Holding
+from astakos.admin import stats as statistics
 
 
 class Command(SynnefoCommand):
@@ -55,7 +49,7 @@ class Command(SynnefoCommand):
     )
 
     def handle(self, *args, **options):
-        stats = get_astakos_stats()
+        stats = statistics.get_astakos_stats()
 
         output_format = options["output_format"]
         if output_format == "json":
@@ -67,49 +61,6 @@ class Command(SynnefoCommand):
                                output_format)
 
 
-def get_astakos_stats():
-    stats = {"datetime": datetime.datetime.now().strftime("%c")}
-
-    resources = Resource.objects.values_list("name", flat=True)
-
-    users = AstakosUser.objects.all()
-    verified = users.filter(email_verified=True)
-    active = users.filter(is_active=True)
-
-    user_stats = {}
-    user_stats["total"] = {"total": users.count(),
-                           "verified": verified.count(),
-                           "active": active.count(),
-                           "usage": {}}
-
-    for resource in resources:
-        usage = Holding.objects.filter(resource=resource)\
-                               .aggregate(summ=Sum("usage_max"))
-        user_stats["total"]["usage"][resource] = int(usage["summ"])
-
-    for provider in settings.ASTAKOS_IM_MODULES:
-
-        users = AstakosUser.objects.filter(auth_providers__module=provider)
-        verified = users.filter(email_verified=True)
-        active = users.filter(is_active=True)
-
-        user_stats[provider] = {"total": users.count(),
-                                "verified": verified.count(),
-                                "active": active.count(),
-                                "usage": {}}
-
-        users_uuids = users.values_list("uuid", flat=True)
-        for resource in resources:
-            usage = Holding.objects\
-                           .filter(holder__in=users_uuids, resource=resource)\
-                           .aggregate(summ=Sum("usage_max"))
-            user_stats[provider]["usage"][resource] = int(usage["summ"])
-
-    stats["users"] = user_stats
-
-    return stats
-
-
 def columns_from_fields(fields, values):
     return zip(map(string.lower, fields), [values.get(f, 0) for f in fields])
 
@@ -117,8 +68,8 @@ def columns_from_fields(fields, values):
 def pretty_print_stats(stats, stdout):
     newline = lambda: stdout.write("\n")
 
-    datetime = stats.get("datetime")
-    stdout.write("datetime: %s\n" % datetime)
+    _datetime = stats.get("datetime")
+    stdout.write("datetime: %s\n" % _datetime)
     newline()
 
     users = stats.get("users", {})
diff --git a/snf-astakos-app/astakos/im/settings.py b/snf-astakos-app/astakos/im/settings.py
index c13a0cd..b1a059d 100644
--- a/snf-astakos-app/astakos/im/settings.py
+++ b/snf-astakos-app/astakos/im/settings.py
@@ -21,6 +21,7 @@ ACCOUNTS_PREFIX = get_path(astakos_services, 'astakos_account.prefix')
 VIEWS_PREFIX = get_path(astakos_services, 'astakos_ui.prefix')
 KEYSTONE_PREFIX = get_path(astakos_services, 'astakos_identity.prefix')
 WEBLOGIN_PREFIX = get_path(astakos_services, 'astakos_weblogin.prefix')
+ADMIN_PREFIX = get_path(astakos_services, 'astakos_admin.prefix')
 
 # Set the expiration time of newly created auth tokens
 # to be this many hours after their creation time.
@@ -207,3 +208,7 @@ KAMAKI_CONFIG_CLOUD_NAME = getattr(settings,
 REDIRECT_ALLOWED_SCHEMES = getattr(settings,
                                    'ASTAKOS_REDIRECT_ALLOWED_SCHEMES',
                                    ('pithos', 'pithosdev'))
+
+ADMIN_STATS_PERMITTED_GROUPS = getattr(settings,
+                                       'ASTAKOS_ADMIN_STATS_PERMITTED_GROUPS',
+                                       ['admin-stats'])
diff --git a/snf-astakos-app/astakos/urls.py b/snf-astakos-app/astakos/urls.py
index 07c09c1..21ad39e 100644
--- a/snf-astakos-app/astakos/urls.py
+++ b/snf-astakos-app/astakos/urls.py
@@ -34,7 +34,8 @@
 from django.conf.urls import include, patterns
 
 from astakos.im.settings import (
-    BASE_PATH, ACCOUNTS_PREFIX, VIEWS_PREFIX, KEYSTONE_PREFIX, WEBLOGIN_PREFIX)
+    BASE_PATH, ACCOUNTS_PREFIX, VIEWS_PREFIX, KEYSTONE_PREFIX, WEBLOGIN_PREFIX,
+    ADMIN_PREFIX)
 from snf_django.lib.api.utils import prefix_pattern
 from snf_django.utils.urls import \
     extend_with_root_redirects, extend_endpoint_with_slash
@@ -52,6 +53,7 @@ astakos_patterns = patterns(
     (prefix_pattern(ACCOUNTS_PREFIX), include('astakos.api.urls')),
     (prefix_pattern(KEYSTONE_PREFIX), include('astakos.api.keystone_urls')),
     (prefix_pattern(WEBLOGIN_PREFIX), include('astakos.im.weblogin_urls')),
+    (prefix_pattern(ADMIN_PREFIX), include('astakos.admin.admin_urls')),
 )
 
 
diff --git a/snf-astakos-app/conf/20-snf-astakos-app-settings.conf b/snf-astakos-app/conf/20-snf-astakos-app-settings.conf
index 3de8f94..07120a1 100644
--- a/snf-astakos-app/conf/20-snf-astakos-app-settings.conf
+++ b/snf-astakos-app/conf/20-snf-astakos-app-settings.conf
@@ -136,3 +136,6 @@
 # Migrate existing shibboleth user entries which where previously associated 
 # with EPPN instead of the provided value of REMOTE_ID mod_shib2 header.
 # ASTAKOS_SHIBBOLETH_MIGRATE_EPPN = False
+#
+## Astakos groups that have access to '/admin' views.
+# ASTAKOS_ADMIN_STATS_PERMITTED_GROUPS = ["admin-stats"]
