From 355d15f9d450c5997ef74b1fb0fc3745bf2dfe35 Mon Sep 17 00:00:00 2001 From: Luke Macken Date: Thu, 24 Jan 2008 15:23:00 -0500 Subject: Only allow localhost and authenticated users access to funcweb. This entails, - Utilizing the TurboGears identity framework - Creating our identity model using SQLAlchemy+Elixir --- funcweb/README.txt | 23 ++++++-- funcweb/dev.cfg | 34 +++++++----- funcweb/funcweb/commands.py | 52 ++++++++++++++++++ funcweb/funcweb/config/app.cfg | 95 +++++++++++++++++++++++++++++++- funcweb/funcweb/controllers.py | 48 ++++++++++++++-- funcweb/funcweb/model.py | 100 +++++++++++++++++++++++++++++++++- funcweb/funcweb/release.py | 9 +++ funcweb/funcweb/templates/master.html | 3 +- funcweb/sample-prod.cfg | 19 ++----- funcweb/setup.py | 28 ++++++---- funcweb/start-funcweb.py | 35 +++++------- funcweb/test.cfg | 32 +++++++++++ 12 files changed, 409 insertions(+), 69 deletions(-) create mode 100644 funcweb/funcweb/commands.py create mode 100644 funcweb/funcweb/release.py create mode 100644 funcweb/test.cfg (limited to 'funcweb') diff --git a/funcweb/README.txt b/funcweb/README.txt index 3374563..4c52dcd 100644 --- a/funcweb/README.txt +++ b/funcweb/README.txt @@ -3,14 +3,29 @@ funcweb A TurboGears interface to func. -This project is currently under development, and should not be used in an -production environment. It employs no concept of security, and should only -be used for testing. +This project is currently under development, and is currently just a +proof-of-concept and should not be used in a production environment. Running ======= - # ./start-funcweb.py + # yum install TurboGears python-genshi python-elixir + $ python setup.py egg_info + $ tg-admin sql create + # ./start-funcweb.py + +Connect to http://localhost:8080 + +Creating a new user +=================== + +Currently funcweb only allows connections from 127.0.0.1 and from authenticated +users. So if you wish to grant other people access to your funcweb instance, +you can create new users easily: + + $ tg-admin shell + >>> user = User(user_name='name', password='password') + >>> ^D Authors ======= diff --git a/funcweb/dev.cfg b/funcweb/dev.cfg index 77efc8c..638c92b 100644 --- a/funcweb/dev.cfg +++ b/funcweb/dev.cfg @@ -1,28 +1,22 @@ [global] # This is where all of your settings go for your development environment # Settings that are the same for both development and production -# (such as template engine, encodings, etc.) all go in +# (such as template engine, encodings, etc.) all go in # funcweb/config/app.cfg # DATABASE +# driver://username:password@host:port/database + # pick the form for your database -# sqlobject.dburi="postgres://username@hostname/databasename" -# sqlobject.dburi="mysql://username:password@hostname:port/databasename" -# sqlobject.dburi="sqlite:///file_name_and_path" +# sqlalchemy.dburi="postgres://username@hostname/databasename" +# sqlalchemy.dburi="mysql://username:password@hostname:port/databasename" +# sqlalchemy.dburi="sqlite://%(current_dir_uri)s/devdata.sqlite" # If you have sqlite, here's a simple default to get you started # in development -sqlobject.dburi="sqlite://%(current_dir_uri)s/devdata.sqlite" - - -# if you are using a database or table type without transactions -# (MySQL default, for example), you should turn off transactions -# by prepending notrans_ on the uri -# sqlobject.dburi="notrans_mysql://username:password@hostname:port/databasename" +sqlalchemy.dburi="sqlite:///devdata.sqlite" -# for Windows users, sqlite URIs look like: -# sqlobject.dburi="sqlite:///drive_letter:/path/to/file" # SERVER @@ -32,6 +26,7 @@ sqlobject.dburi="sqlite://%(current_dir_uri)s/devdata.sqlite" # Enable the debug output at the end on pages. # log_debug_info_filter.on = False +server.socket_host="127.0.0.1" server.environment="development" autoreload.package="funcweb" @@ -64,3 +59,16 @@ level='INFO' qualname='turbogears.access' handlers=['access_out'] propagate=0 + +[[[identity]]] +level='INFO' +qualname='turbogears.identity' +handlers=['access_out'] +propagate=0 + +[[[database]]] +# Set to INFO to make SQLAlchemy display SQL commands +level='ERROR' +qualname='sqlalchemy.engine' +handlers=['debug_out'] +propagate=0 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 @@ -# +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 @@
+
diff --git a/funcweb/sample-prod.cfg b/funcweb/sample-prod.cfg index 6ca2b4e..7f6cc6e 100644 --- a/funcweb/sample-prod.cfg +++ b/funcweb/sample-prod.cfg @@ -8,23 +8,16 @@ # DATABASE +# driver://username:password@host:port/database + # pick the form for your database -# sqlobject.dburi="postgres://username@hostname/databasename" -# sqlobject.dburi="mysql://username:password@hostname:port/databasename" -# sqlobject.dburi="sqlite:///file_name_and_path" +# sqlalchemy.dburi="postgres://username@hostname/databasename" +# sqlalchemy.dburi="mysql://username:password@hostname:port/databasename" +# sqlalchemy.dburi="sqlite:///file_name_and_path" # If you have sqlite, here's a simple default to get you started # in development -sqlobject.dburi="sqlite://%(current_dir_uri)s/devdata.sqlite" - - -# if you are using a database or table type without transactions -# (MySQL default, for example), you should turn off transactions -# by prepending notrans_ on the uri -# sqlobject.dburi="notrans_mysql://username:password@hostname:port/databasename" - -# for Windows users, sqlite URIs look like: -# sqlobject.dburi="sqlite:///drive_letter:/path/to/file" +sqlalchemy.dburi="sqlite:///%(current_dir_uri)s/devdata.sqlite" # SERVER diff --git a/funcweb/setup.py b/funcweb/setup.py index 7ba1a53..9bde340 100644 --- a/funcweb/setup.py +++ b/funcweb/setup.py @@ -1,7 +1,10 @@ +# -*- coding: utf-8 -*- + from setuptools import setup, find_packages from turbogears.finddata import find_package_data import os +execfile(os.path.join("funcweb", "release.py")) packages=find_packages() package_data = find_package_data(where='funcweb', @@ -14,18 +17,16 @@ if os.path.isdir('locales'): setup( name="funcweb", version=version, - - #description=description, - author="Luke Macken", - author_email="lmacken@redhat.com", - #url=url, - #download_url=download_url, - #license=license, + description=description, + author=author, + author_email=email, + url=url, + license=license, install_requires=[ - "TurboGears >= 1.0.3.2", + "TurboGears >= 1.0.4.2", + "SQLAlchemy>=0.3.10", ], - scripts=["start-funcweb.py"], zip_safe=False, packages=packages, package_data=package_data, @@ -63,5 +64,12 @@ setup( # 'Framework :: TurboGears :: Widgets', ], test_suite='nose.collector', + entry_points = { + 'console_scripts': [ + 'start-funcweb = funcweb.commands:start', + ], + }, + # Uncomment next line and create a default.cfg file in your project dir + # if you want to package a default configuration in your egg. + #data_files = [('config', ['default.cfg'])], ) - diff --git a/funcweb/start-funcweb.py b/funcweb/start-funcweb.py index 604cf19..3d375a3 100755 --- a/funcweb/start-funcweb.py +++ b/funcweb/start-funcweb.py @@ -1,25 +1,18 @@ #!/usr/bin/python -__requires__="TurboGears" -import pkg_resources +# -*- coding: utf-8 -*- +"""Start script for the funcweb TurboGears project. -from turbogears import config, update_config, start_server -import cherrypy -cherrypy.lowercase_api = True -from os.path import * -import sys +This script is only needed during development for running from the project +directory. When the project is installed, easy_install will create a +proper start script. +""" -# 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 this directory. If it's not there, this script is -# probably installed -if len(sys.argv) > 1: - update_config(configfile=sys.argv[1], - modulename="funcweb.config") -elif exists(join(dirname(__file__), "setup.py")): - update_config(configfile="dev.cfg",modulename="funcweb.config") -else: - update_config(configfile="prod.cfg",modulename="funcweb.config") -config.update(dict(package="funcweb")) +import sys +from funcweb.commands import start, ConfigurationError -from funcweb.controllers import Root -start_server(Root()) +if __name__ == "__main__": + try: + start() + except ConfigurationError, exc: + sys.stderr.write(str(exc)) + sys.exit(1) diff --git a/funcweb/test.cfg b/funcweb/test.cfg new file mode 100644 index 0000000..4140d5a --- /dev/null +++ b/funcweb/test.cfg @@ -0,0 +1,32 @@ +[global] +# You can place test-specific configuration options here (like test db uri, etc) + +# DATABASE + +sqlalchemy.dburi = "sqlite:///:memory:" + +# LOGGING + +[logging] + +[[formatters]] +[[[full_content]]] +format='*(asctime)s *(name)s *(levelname)s *(message)s' + +[[handlers]] +[[[test_out]]] +class='StreamHandler' +level='DEBUG' +args='(sys.stdout,)' +formatter='full_content' + +[[loggers]] +[[[funcweb]]] +level='DEBUG' +qualname='funcweb' +handlers=['test_out'] + +[[[turbogears]]] +level='INFO' +qualname='turbogears' +handlers=['test_out'] -- cgit