From ad06135abee6b60aedc0c0799e5f02d00fed1fa2 Mon Sep 17 00:00:00 2001 From: Aleksey Lim Date: Mon, 01 Oct 2012 18:05:35 +0000 Subject: Initial implemenation for auth roles --- diff --git a/sugar_network/node/auth.py b/sugar_network/node/auth.py new file mode 100644 index 0000000..fdb7975 --- /dev/null +++ b/sugar_network/node/auth.py @@ -0,0 +1,47 @@ +# Copyright (C) 2012 Aleksey Lim +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +from ConfigParser import ConfigParser +from os.path import join, exists + +import active_document as ad +from sugar_network import node +from active_toolkit import enforce + + +_config = None + + +def validate(request, role): + enforce(_validate(request.principal, role), ad.Forbidden, + 'No enough permissions to proceed the operation') + + +def try_validate(request, role): + return _validate(request.principal, role) or False + + +def _validate(user, role): + global _config + + if _config is None: + _config = ConfigParser() + config_path = join(node.data_root.value, 'authorization.conf') + if exists(config_path): + _config.read(config_path) + + if _config.has_option(user, role): + return _config.get(user, role).strip().lower() in \ + ('true', 'on', '1', 'allow') diff --git a/sugar_network/node/commands.py b/sugar_network/node/commands.py index ba5b6ed..1df1a13 100644 --- a/sugar_network/node/commands.py +++ b/sugar_network/node/commands.py @@ -21,6 +21,7 @@ from os.path import exists, join import active_document as ad from sugar_network import node from sugar_network.node.sync_master import SyncCommands +from sugar_network.node import auth from sugar_network.resources.volume import Commands from sugar_network.toolkit import router from active_toolkit import util, enforce @@ -95,7 +96,8 @@ class NodeCommands(ad.VolumeCommands, Commands): if cmd.permissions & ad.ACCESS_AUTHOR and 'guid' in request: doc = self.volume[request['document']].get(request['guid']) - enforce(request.principal in doc['user'], ad.Forbidden, + enforce(request.principal in doc['user'] or + auth.try_validate(request, 'root'), ad.Forbidden, 'Operation is permitted only for authors') return cmd @@ -166,6 +168,7 @@ class MasterCommands(NodeCommands, SyncCommands): @ad.document_command(method='PUT', cmd='merge', permissions=ad.ACCESS_AUTH) def merge(self, document, guid, request): + auth.validate(request, 'root') directory = self.volume[document] directory.merge(guid, request.content) diff --git a/sugar_network/resources/context.py b/sugar_network/resources/context.py index 5683664..8af986c 100644 --- a/sugar_network/resources/context.py +++ b/sugar_network/resources/context.py @@ -20,7 +20,7 @@ from sugar_network import resources, static from sugar_network.local import activities from sugar_network.resources.volume import Resource from sugar_network.zerosugar import Spec -from sugar_network.node import obs +from sugar_network.node import obs, auth from active_toolkit import coroutine, util @@ -158,19 +158,21 @@ class Context(Resource): def presolve(self, value): return value - @classmethod - @ad.directory_command(method='PUT', cmd='include', - arguments={'layers': ad.to_list}) - def include(cls, directory, layers, request): - import logging - logging.error('include> %r %r', layers, request.content) - - @classmethod - @ad.directory_command(method='PUT', cmd='exclude', - arguments={'layers': ad.to_list}) - def exclude(cls, directory, layers, request): - import logging - logging.error('exclude> %r %r', layers, request.content) + @ad.document_command(method='PUT', cmd='attach', + permissions=ad.ACCESS_AUTH) + def attach(self, request): + auth.validate(request, 'root') + # TODO Reading layer here is a race + layer = list(set(self['layer']) | set(request.content)) + request.volume['context'].update(self.guid, {'layer': layer}) + + @ad.document_command(method='PUT', cmd='detach', + permissions=ad.ACCESS_AUTH) + def detach(self, request): + auth.validate(request, 'root') + # TODO Reading layer here is a race + layer = list(set(self['layer']) - set(request.content)) + request.volume['context'].update(self.guid, {'layer': layer}) def _process_aliases(self, aliases): packages = {} diff --git a/sugar_network/resources/volume.py b/sugar_network/resources/volume.py index b7680a1..bf5d122 100644 --- a/sugar_network/resources/volume.py +++ b/sugar_network/resources/volume.py @@ -36,6 +36,7 @@ _logger = logging.getLogger('resources.volume') class Request(ad.Request): + principal = None mountpoint = None diff --git a/sugar_network/toolkit/router.py b/sugar_network/toolkit/router.py index 88113cd..29c8192 100644 --- a/sugar_network/toolkit/router.py +++ b/sugar_network/toolkit/router.py @@ -249,7 +249,6 @@ class _Request(Request): environ = None url = None path = None - principal = None def __init__(self, environ=None): Request.__init__(self) diff --git a/tests/__init__.py b/tests/__init__.py index 9d7359a..30193fc 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -27,7 +27,7 @@ from sugar_network.resources.user import User from sugar_network.resources.context import Context from sugar_network.node.commands import NodeCommands, MasterCommands from sugar_network.node.router import Router as MasterRouter -from sugar_network.node import stats, obs +from sugar_network.node import stats, obs, auth from sugar_network.resources.volume import Volume @@ -90,6 +90,7 @@ class Test(unittest.TestCase): stats._cache.clear() obs._client = None http._RECONNECTION_NUMBER = 0 + auth._config = None Volume.RESOURCES = [ 'sugar_network.resources.user', @@ -114,6 +115,7 @@ class Test(unittest.TestCase): self.server = None self.mounts = None + self.volume = None self.forks = [] self.fork_num = fork_num @@ -134,6 +136,9 @@ class Test(unittest.TestCase): pid = self.forks.pop() self.assertEqual(0, self.waitpid(pid)) coroutine.shutdown() + if self.volume is not None: + self.volume.close() + self.volume = None def waitpid(self, pid, sig=signal.SIGTERM): if pid in self.forks: @@ -273,12 +278,12 @@ class Test(unittest.TestCase): if classes is None: classes = [User, Context] self.touch('master/master') - volume = Volume('master', classes) - cp = MasterCommands(volume) + self.volume = Volume('master', classes) + cp = MasterCommands(self.volume) self.server = coroutine.WSGIServer(('localhost', 8800), MasterRouter(cp)) coroutine.spawn(self.server.serve_forever) coroutine.dispatch() - return volume + return self.volume def sign(privkey, data): diff --git a/tests/units/__main__.py b/tests/units/__main__.py index da752b3..5e3f83d 100644 --- a/tests/units/__main__.py +++ b/tests/units/__main__.py @@ -22,6 +22,7 @@ from remote_mount import * from node_mount import * from injector import * from mountset import * +from auth import * from context import * from implementation import * from obs import * diff --git a/tests/units/auth.py b/tests/units/auth.py new file mode 100755 index 0000000..e499f35 --- /dev/null +++ b/tests/units/auth.py @@ -0,0 +1,70 @@ +#!/usr/bin/env python +# sugar-lint: disable + +from __init__ import tests + +import active_document as ad +from sugar_network.node import auth +from sugar_network import IPCClient, Client +from sugar_network.resources.volume import Request +from active_toolkit import enforce + + +class AuthTest(tests.Test): + + def test_Config(self): + self.touch(('authorization.conf', [ + '[user_1]', + 'role_1 = True', + '[user_2]', + 'role_2 = False', + ])) + + request = Request() + request.principal = 'user_1' + self.assertEqual(True, auth.try_validate(request, 'role_1')) + auth.validate(request, 'role_1') + + request.principal = 'user_2' + self.assertEqual(False, auth.try_validate(request, 'role_2')) + self.assertRaises(ad.Forbidden, auth.validate, request, 'role_2') + + request.principal = 'user_3' + self.assertEqual(False, auth.try_validate(request, 'role_1')) + self.assertEqual(False, auth.try_validate(request, 'role_2')) + self.assertRaises(ad.Forbidden, auth.validate, request, 'role_1') + self.assertRaises(ad.Forbidden, auth.validate, request, 'role_2') + + def test_FullWriteForRoot(self): + client = Client() + + self.start_master() + client.post(['context'], { + 'implement': 'guid', + 'type': 'package', + 'title': 'title', + 'summary': 'summary', + 'description': 'description', + }) + self.assertNotEqual('probe', client.get(['context', 'guid', 'title'])) + self.stop_servers() + + self.touch(( + 'master/context/gu/guid/user', + '{"seqno": 1, "value": ["fake"]}', + )) + + self.start_master() + self.assertRaises(RuntimeError, client.put, ['context', 'guid'], {'title': 'probe'}) + + self.touch(('authorization.conf', [ + '[%s]' % tests.UID, + 'root = True', + ])) + auth._config = None + client.put(['context', 'guid'], {'title': 'probe'}) + self.assertEqual('probe', client.get(['context', 'guid', 'title'])) + + +if __name__ == '__main__': + tests.main() -- cgit v0.9.1