diff options
author | Luke Macken <lmacken@redhat.com> | 2008-01-24 15:23:00 -0500 |
---|---|---|
committer | Luke Macken <lmacken@redhat.com> | 2008-01-24 15:23:00 -0500 |
commit | 355d15f9d450c5997ef74b1fb0fc3745bf2dfe35 (patch) | |
tree | 824a4c121efa2e4e63dee3caefa2feb7dffb81f5 /funcweb/funcweb | |
parent | df4da5b1811b108b22cf6a4cab5e2fe5d75ef806 (diff) | |
download | func-355d15f9d450c5997ef74b1fb0fc3745bf2dfe35.tar.gz func-355d15f9d450c5997ef74b1fb0fc3745bf2dfe35.tar.xz func-355d15f9d450c5997ef74b1fb0fc3745bf2dfe35.zip |
Only allow localhost and authenticated users access to funcweb. This entails,
- Utilizing the TurboGears identity framework
- Creating our identity model using SQLAlchemy+Elixir
Diffstat (limited to 'funcweb/funcweb')
-rw-r--r-- | funcweb/funcweb/commands.py | 52 | ||||
-rw-r--r-- | funcweb/funcweb/config/app.cfg | 95 | ||||
-rw-r--r-- | funcweb/funcweb/controllers.py | 48 | ||||
-rw-r--r-- | funcweb/funcweb/model.py | 100 | ||||
-rw-r--r-- | funcweb/funcweb/release.py | 9 | ||||
-rw-r--r-- | funcweb/funcweb/templates/master.html | 3 |
6 files changed, 299 insertions, 8 deletions
diff --git a/funcweb/funcweb/commands.py b/funcweb/funcweb/commands.py new file mode 100644 index 0000000..043ce1f --- /dev/null +++ b/funcweb/funcweb/commands.py @@ -0,0 +1,52 @@ +# -*- coding: utf-8 -*- +"""This module contains functions called from console script entry points.""" + +import os +import sys + +from os.path import dirname, exists, join + +import pkg_resources +pkg_resources.require("TurboGears") + +import turbogears +import cherrypy + +cherrypy.lowercase_api = True + +class ConfigurationError(Exception): + pass + +def start(): + """Start the CherryPy application server.""" + + setupdir = dirname(dirname(__file__)) + curdir = os.getcwd() + + # First look on the command line for a desired config file, + # if it's not on the command line, then look for 'setup.py' + # in the current directory. If there, load configuration + # from a file called 'dev.cfg'. If it's not there, the project + # is probably installed and we'll look first for a file called + # 'prod.cfg' in the current directory and then for a default + # config file called 'default.cfg' packaged in the egg. + if len(sys.argv) > 1: + configfile = sys.argv[1] + elif exists(join(setupdir, "setup.py")): + configfile = join(setupdir, "dev.cfg") + elif exists(join(curdir, "prod.cfg")): + configfile = join(curdir, "prod.cfg") + else: + try: + configfile = pkg_resources.resource_filename( + pkg_resources.Requirement.parse("funcweb"), + "config/default.cfg") + except pkg_resources.DistributionNotFound: + raise ConfigurationError("Could not find default configuration.") + + turbogears.update_config(configfile=configfile, + modulename="funcweb.config") + + from funcweb.controllers import Root + + turbogears.start_server(Root()) diff --git a/funcweb/funcweb/config/app.cfg b/funcweb/funcweb/config/app.cfg index 24e0807..504f018 100644 --- a/funcweb/funcweb/config/app.cfg +++ b/funcweb/funcweb/config/app.cfg @@ -19,16 +19,109 @@ tg.defaultview = "genshi" # Allow every exposed function to be called as json, # tg.allow_json = False +# Suppress the inclusion of the shipped MochiKit version, which is rather outdated. +# Attention: setting this to True and listing 'turbogears.mochikit' in 'tg.include_widgets' +# is a contradiction. This option will overrule the default-inclusion to prevent version +# mismatch bugs. +# tg.mochikit_suppress = True + # List of Widgets to include on every page. -# for exemple ['turbogears.mochikit'] +# for example ['turbogears.mochikit'] # tg.include_widgets = [] # Set to True if the scheduler should be started # tg.scheduler = False +# Set to True to allow paginate decorator redirects when page number gets +# out of bound. Useful for getting the real page id in the url +# paginate.redirect_on_out_of_range = True + +# Set to True to allow paginate decorator redirects when last page is requested. +# This is useful for getting the real last page id in the url +# paginate.redirect_on_last_page = True + # Set session or cookie # session_filter.on = True +# VISIT TRACKING +# Each visit to your application will be assigned a unique visit ID tracked via +# a cookie sent to the visitor's browser. +# -------------- + +# Enable Visit tracking +visit.on=True + +# Number of minutes a visit may be idle before it expires. +# visit.timeout=20 + +# The name of the cookie to transmit to the visitor's browser. +# visit.cookie.name="tg-visit" + +# Domain name to specify when setting the cookie (must begin with . according to +# RFC 2109). The default (None) should work for most cases and will default to +# the machine to which the request was made. NOTE: localhost is NEVER a valid +# value and will NOT WORK. +# visit.cookie.domain=None + +# Specific path for the cookie +# visit.cookie.path="/" + +# The name of the VisitManager plugin to use for visitor tracking. +visit.manager="sqlalchemy" + +# Database class to use for visit tracking +visit.saprovider.model = "funcweb.model.Visit" +identity.saprovider.model.visit = "funcweb.model.VisitIdentity" + +# IDENTITY +# General configuration of the TurboGears Identity management module +# -------- + +# Switch to turn on or off the Identity management module +identity.on=True + +# [REQUIRED] URL to which CherryPy will internally redirect when an access +# control check fails. If Identity management is turned on, a value for this +# option must be specified. +identity.failure_url="/login" + +identity.provider='sqlalchemy' + +# The names of the fields on the login form containing the visitor's user ID +# and password. In addition, the submit button is specified simply so its +# existence may be stripped out prior to passing the form data to the target +# controller. +# identity.form.user_name="user_name" +# identity.form.password="password" +# identity.form.submit="login" + +# What sources should the identity provider consider when determining the +# identity associated with a request? Comma separated list of identity sources. +# Valid sources: form, visit, http_auth +# identity.source="form,http_auth,visit" + +# SqlAlchemyIdentityProvider +# Configuration options for the default IdentityProvider +# ------------------------- + +# The classes you wish to use for your Identity model. Remember to not use reserved +# SQL keywords for class names (at least unless you specify a different table +# name using sqlmeta). +identity.saprovider.model.user="funcweb.model.User" +identity.saprovider.model.group="funcweb.model.Group" +identity.saprovider.model.permission="funcweb.model.Permission" + +# The password encryption algorithm used when comparing passwords against what's +# stored in the database. Valid values are 'md5' or 'sha1'. If you do not +# specify an encryption algorithm, passwords are expected to be clear text. +# The SqlAlchemyProvider *will* encrypt passwords supplied as part of your login +# form. If you set the password through the password property, like: +# my_user.password = 'secret' +# the password will be encrypted in the database, provided identity is up and +# running, or you have loaded the configuration specifying what encryption to +# use (in situations where identity may not yet be running, like tests). + +# identity.saprovider.encryption_algorithm=None # compress the data sends to the web browser # [/] diff --git a/funcweb/funcweb/controllers.py b/funcweb/funcweb/controllers.py index 0d74a50..df4c05c 100644 --- a/funcweb/funcweb/controllers.py +++ b/funcweb/funcweb/controllers.py @@ -1,18 +1,24 @@ import logging log = logging.getLogger(__name__) -from turbogears import controllers, expose, flash +from turbogears import controllers, expose, flash, identity, redirect from func.overlord.client import Client class Root(controllers.RootController): @expose(template="funcweb.templates.minions") - def minions(self): - """ Return a list of our minions """ - fc = Client("*") + @identity.require(identity.Any( + identity.from_host("127.0.0.1"), identity.not_anonymous())) + def minions(self, glob='*'): + """ Return a list of our minions that match a given glob """ + fc = Client(glob) return dict(minions=fc.system.list_methods()) + index = minions # start with our minion view, for now + @expose(template="funcweb.templates.minion") + @identity.require(identity.Any( + identity.from_host("127.0.0.1"), identity.not_anonymous())) def minion(self, name, module=None, method=None): """ Display module or method details for a specific minion. @@ -34,11 +40,43 @@ class Root(controllers.RootController): return dict(modules=modules, module=module, tg_template="funcweb.templates.module") - index = minions # start with our minion view, for now @expose(template="funcweb.templates.run") + @identity.require(identity.Any( + identity.from_host("127.0.0.1"), identity.not_anonymous())) def run(self, minion="*", module=None, method=None, arguments=''): fc = Client(minion) results = getattr(getattr(fc, module), method)(*arguments.split()) cmd = "%s.%s.%s(%s)" % (minion, module, method, arguments) return dict(cmd=cmd, results=results) + + @expose(template="funcweb.templates.login") + def login(self, forward_url=None, previous_url=None, *args, **kw): + from cherrypy import request, response + if not identity.current.anonymous \ + and identity.was_login_attempted() \ + and not identity.get_identity_errors(): + raise redirect(forward_url) + + forward_url=None + previous_url= request.path + + if identity.was_login_attempted(): + msg=_("The credentials you supplied were not correct or " + "did not grant access to this resource.") + elif identity.get_identity_errors(): + msg=_("You must provide your credentials before accessing " + "this resource.") + else: + msg=_("Please log in.") + forward_url= request.headers.get("Referer", "/") + + response.status=403 + return dict(message=msg, previous_url=previous_url, logging_in=True, + original_parameters=request.params, + forward_url=forward_url) + + @expose() + def logout(self): + identity.current.logout() + raise redirect("/") diff --git a/funcweb/funcweb/model.py b/funcweb/funcweb/model.py index c35e930..2997cf0 100644 --- a/funcweb/funcweb/model.py +++ b/funcweb/funcweb/model.py @@ -1 +1,99 @@ -# <insert SQLAlchemy hotness here> +from datetime import datetime +# import the basic Elixir classes and functions for declaring the data model +# (see http://elixir.ematia.de/trac/wiki/TutorialDivingIn) +from elixir import Entity, Field, OneToMany, ManyToOne, ManyToMany +from elixir import options_defaults, using_options, setup_all +# import some datatypes for table columns from Elixir +# (see http://www.sqlalchemy.org/docs/04/types.html for more) +from elixir import String, Unicode, Integer, DateTime +from turbogears import identity + +options_defaults['autosetup'] = False + + +# your data model + +# class YourDataClass(Entity): +# pass + + +# the identity model + + +class Visit(Entity): + """ + A visit to your site + """ + using_options(tablename='visit') + + visit_key = Field(String(40), primary_key=True) + created = Field(DateTime, nullable=False, default=datetime.now) + expiry = Field(DateTime) + + @classmethod + def lookup_visit(cls, visit_key): + return Visit.get(visit_key) + + +class VisitIdentity(Entity): + """ + A Visit that is link to a User object + """ + using_options(tablename='visit_identity') + + visit_key = Field(String(40), primary_key=True) + user = ManyToOne('User', colname='user_id', use_alter=True) + + +class Group(Entity): + """ + An ultra-simple group definition. + """ + using_options(tablename='tg_group') + + group_id = Field(Integer, primary_key=True) + group_name = Field(Unicode(16), unique=True) + display_name = Field(Unicode(255)) + created = Field(DateTime, default=datetime.now) + users = ManyToMany('User', tablename='user_group') + permissions = ManyToMany('Permission', tablename='group_permission') + + +class User(Entity): + """ + Reasonably basic User definition. + Probably would want additional attributes. + """ + using_options(tablename='tg_user') + + user_id = Field(Integer, primary_key=True) + user_name = Field(Unicode(16), unique=True) + email_address = Field(Unicode(255), unique=True) + display_name = Field(Unicode(255)) + password = Field(Unicode(40)) + created = Field(DateTime, default=datetime.now) + groups = ManyToMany('Group', tablename='user_group') + + @property + def permissions(self): + perms = set() + for g in self.groups: + perms |= set(g.permissions) + return perms + + +class Permission(Entity): + """ + A relationship that determines what each Group can do + """ + using_options(tablename='permission') + + permission_id = Field(Integer, primary_key=True) + permission_name = Field(Unicode(16), unique=True) + description = Field(Unicode(255)) + groups = ManyToMany('Group', tablename='group_permission') + + +# Set up all Elixir entities declared above + +setup_all() diff --git a/funcweb/funcweb/release.py b/funcweb/funcweb/release.py new file mode 100644 index 0000000..b6593d4 --- /dev/null +++ b/funcweb/funcweb/release.py @@ -0,0 +1,9 @@ +# Release information about funcweb + +version = "0.1" +description = "A web interface to func" +author = "Luke Macken" +email = "lmacken@redhat.com" +copyright = "2008 Red Hat, Inc." +url = "http://fedorahosted.org/func" +license = "GPL" diff --git a/funcweb/funcweb/templates/master.html b/funcweb/funcweb/templates/master.html index 8d09254..ab14ca0 100644 --- a/funcweb/funcweb/templates/master.html +++ b/funcweb/funcweb/templates/master.html @@ -34,6 +34,7 @@ </div> </div> <div class="content"> + <!-- <div py:if="tg.config('identity.on',False) and not 'logging_in' in locals()" id="pageLogin" class="usernav"> <span py:if="tg.identity.anonymous"> You are not logged in yet <a class="loginButton" href="${tg.url('/login/')}">login</a> @@ -43,7 +44,7 @@ <a class="loginButton" href="${tg.url('/logout/')}">logout</a> </span> </div> - + --> <div py:replace="select('*|text()')" /> </div> </div> |