21:授权保护资源¶
将安全语句分配给描述执行操作所需权限的资源。
背景¶
我们的应用程序有允许人们通过Web浏览器添加/编辑/删除内容的URL。向应用程序添加安全性的时间。让我们保护添加/编辑视图以要求登录(用户名为 editor
密码 editor
)我们将允许其他视图在没有密码的情况下继续工作。
目标¶
介绍认证、授权、权限和访问控制列表(ACL)的Pyramid概念。
做一个 root factory 返回应用程序顶部的类实例。
将安全语句分配给根资源。
在视图上添加权限谓词。
提供一个 Forbidden view 处理访问没有足够权限的URL。
步骤¶
我们将使用验证步骤作为起点:
cd ..; cp -r authentication authorization; cd authorization $VENV/bin/pip install -e .
从改变开始
authorization/tutorial/__init__.py
将根工厂指定给 configurator :1from pyramid.config import Configurator 2 3from .security import SecurityPolicy 4 5 6def main(global_config, **settings): 7 config = Configurator(settings=settings, 8 root_factory='.resources.Root') 9 config.include('pyramid_chameleon') 10 11 config.set_security_policy( 12 SecurityPolicy( 13 secret=settings['tutorial.secret'], 14 ), 15 ) 16 17 config.add_route('home', '/') 18 config.add_route('hello', '/howdy') 19 config.add_route('login', '/login') 20 config.add_route('logout', '/logout') 21 config.scan('.views') 22 return config.make_wsgi_app()
这意味着我们需要实施
authorization/tutorial/resources.py
:1from pyramid.authorization import Allow, Everyone 2 3 4class Root: 5 __acl__ = [(Allow, Everyone, 'view'), 6 (Allow, 'group:editors', 'edit')] 7 8 def __init__(self, request): 9 pass
定义一个
GROUPS
数据存储和permits
我们的方法SecurityPolicy
:1import bcrypt 2from pyramid.authentication import AuthTktCookieHelper 3from pyramid.authorization import ( 4 ACLHelper, 5 Authenticated, 6 Everyone, 7) 8 9 10def hash_password(pw): 11 pwhash = bcrypt.hashpw(pw.encode('utf8'), bcrypt.gensalt()) 12 return pwhash.decode('utf8') 13 14def check_password(pw, hashed_pw): 15 expected_hash = hashed_pw.encode('utf8') 16 return bcrypt.checkpw(pw.encode('utf8'), expected_hash) 17 18 19USERS = {'editor': hash_password('editor'), 20 'viewer': hash_password('viewer')} 21GROUPS = {'editor': ['group:editors']} 22 23 24class SecurityPolicy: 25 def __init__(self, secret): 26 self.authtkt = AuthTktCookieHelper(secret=secret) 27 self.acl = ACLHelper() 28 29 def identity(self, request): 30 identity = self.authtkt.identify(request) 31 if identity is not None and identity['userid'] in USERS: 32 return identity 33 34 def authenticated_userid(self, request): 35 identity = self.identity(request) 36 if identity is not None: 37 return identity['userid'] 38 39 def remember(self, request, userid, **kw): 40 return self.authtkt.remember(request, userid, **kw) 41 42 def forget(self, request, **kw): 43 return self.authtkt.forget(request, **kw) 44 45 def permits(self, request, context, permission): 46 principals = self.effective_principals(request) 47 return self.acl.permits(context, principals, permission) 48 49 def effective_principals(self, request): 50 principals = [Everyone] 51 userid = self.authenticated_userid(request) 52 if userid is not None: 53 principals += [Authenticated, 'u:' + userid] 54 principals += GROUPS.get(userid, []) 55 return principals
变化
authorization/tutorial/views.py
要求edit
许可hello
查看和执行禁止的视图:1from pyramid.httpexceptions import HTTPFound 2from pyramid.security import ( 3 remember, 4 forget, 5) 6 7from pyramid.view import ( 8 view_config, 9 view_defaults, 10 forbidden_view_config 11) 12 13from .security import ( 14 USERS, 15 check_password 16) 17 18 19@view_defaults(renderer='home.pt') 20class TutorialViews: 21 def __init__(self, request): 22 self.request = request 23 self.logged_in = request.authenticated_userid 24 25 @view_config(route_name='home') 26 def home(self): 27 return {'name': 'Home View'} 28 29 @view_config(route_name='hello', permission='edit') 30 def hello(self): 31 return {'name': 'Hello View'} 32 33 @view_config(route_name='login', renderer='login.pt') 34 @forbidden_view_config(renderer='login.pt') 35 def login(self): 36 request = self.request 37 login_url = request.route_url('login') 38 referrer = request.url 39 if referrer == login_url: 40 referrer = '/' # never use login form itself as came_from 41 came_from = request.params.get('came_from', referrer) 42 message = '' 43 login = '' 44 password = '' 45 if 'form.submitted' in request.params: 46 login = request.params['login'] 47 password = request.params['password'] 48 hashed_pw = USERS.get(login) 49 if hashed_pw and check_password(password, hashed_pw): 50 headers = remember(request, login) 51 return HTTPFound(location=came_from, 52 headers=headers) 53 message = 'Failed login' 54 55 return dict( 56 name='Login', 57 message=message, 58 url=request.application_url + '/login', 59 came_from=came_from, 60 login=login, 61 password=password, 62 ) 63 64 @view_config(route_name='logout') 65 def logout(self): 66 request = self.request 67 headers = forget(request) 68 url = request.route_url('home') 69 return HTTPFound(location=url, 70 headers=headers)
运行 Pyramid 应用程序时使用:
$VENV/bin/pserve development.ini --reload
在浏览器中打开http://localhost:6543/。
如果您仍在登录,请单击“注销”链接。
在浏览器中访问http://localhost:6543/howdy。应该要求您登录。
分析¶
这个简单的教程步骤可以归结为以下几步:
视图可能需要 许可 (
edit
)我们观点的背景
Root
)具有访问控制列表(ACL)。这个acl说
edit
权限可用于Root
到group:editors
主要的 .这个
SecurityPolicy.effective_principals
方法回答特定用户是否 (editor
)是一个特定群体的成员 (group:editors
)这个
SecurityPolicy.permits
方法在Pyramid希望知道是否允许用户执行某些操作时调用。为此,它使用pyramid.authorization.ACLHelper
检查ACLcontext
并确定请求是允许还是拒绝特定权限。
综上所述, hello
欲望 edit
许可, Root
说 group:editors
有 edit
许可。
当然,这只适用于 Root
. 网站的其他部分(A.K.A. 语境 )可能有不同的ACL。
如果您没有登录并访问 /howdy
,您需要显示登录屏幕。Pyramid如何知道要使用的登录页面是什么?我们明确地告诉Pyramid login
视图应通过使用 @forbidden_view_config
.
额外credit¶
用户和主体有什么区别?
我可以用数据库代替
GROUPS
数据存储以查找主体?我必须放一个
renderer
在我的@forbidden_view_config
装饰师?也许您希望没有足够权限(禁止)的体验变得更丰富。你怎么能改变这个?
也许我们希望将安全语句存储在数据库中,并允许通过浏览器进行编辑。怎么办?
如果我们想要在不同类型的对象上使用不同的安全性语句呢?或者在同一类对象上,但在URL层次结构的不同部分?