Зачем ето нужно ?
В идеальном мире мы не должны были бы делать вещи, которые нам не полагается делать. Но так как наш мир не идеален, иногда люди занимаются нелегальными делами. Это все также применимо и к компьютерному миру, к компьютерным системам, которые используются больше, чем одним пользователем. Почти все пытаются прочитать чужую почту, просмотреть отчеты о жаловании или получить доступ к скрытой информации.
Я знаю, Вы этого никогда не делали, но многие этим занимаются.
Простейшие методы
Самый простой способ разрешить или запретить определенные действия для учетной записи пользователя – это проверять, находится ли данная учетная запись в списке тех, кому данные действия позволены. Если Вы хотите запретить все действия кроме явно позволенных, функция доступа может быть достаточно простой, например:
# access_check() return 1 or undef sub access_check { my $user_id = shift; my @allow_users = @_; my %quick_allow = map { $_ => 1 } @allow_users; return $quick_allow{ $user_id }; } my @allowed = ( 11, 12, 23, 45 ); print "User 23 allowed\n" if access_check( 23, @allowed ); print "User 13 allowed\n" if access_check( 13, @allowed ); print "User 99 allowed\n" if access_check( 99, @allowed ); # only "User 23 allowed" will be printed
Обычно контроль доступа настолько же прост, как и эта функция. Использование ID пользователей для контроля доступа – это просто, но иногда такой метод сложно поддерживать и обслуживать. Проблема возникает в системах с большим количеством пользователей или же в системах свободного доступа, где в любой момент могут быть созданы новые пользователи. Для каждой функции, которая требует контроля, список доступа становится огромным.
Одним из методов решения данной проблемы состоит в создание групп доступа. Причем, каждый пользователь может принадлежать к нескольким группам одновременно. Если группа, в которой состоит данный пользователь, имеет права доступа к некой функции, то проверка на контроль доступа считается успешной.
# mimic real system environment: # %ALL_USER_GROUPS represents "storage" that contains all # groups that each user is attached to my %ALL_USER_GROUPS = ( 23 => [ qw( g1 g4 ) ], 13 => [ qw( g3 g5 ) ], ); # user 23 is in groups g1 and g4 # user 13 -- in g3 and g5 # return list of user's groups. read data from storage or # from %ALL_USER_GROUPS in this example sub get_user_groups { my $user_id = shift; return @{ $ALL_USER_GROUPS{ $user_id } || [] }; } # access_check_grp() return 1 or 0 sub access_check_grp { my $user_id = shift; my @allow_users = @_; my %quick_allow = map { $_ => 1 } @allow_users; my @user_groups = get_user_groups( $user_id ); for my $group ( @user_groups ) { # user groups is listed, allow return 1 if $quick_allow{ $group }; } # user group not found, deny return 0; } # this groups list is static and will not be altered # when users are added or removed from the system my @allowed = qw( g1 g2 g7 g9 ); print "User 23 allowed\n" if access_check_grp( 23, @allowed ); print "User 13 allowed\n" if access_check_grp( 13, @allowed ); print "User 99 allowed\n" if access_check_grp( 99, @allowed ); # only "User 23 allowed" will be printed
Хранение
Наверное, наиболее популярные хранилища для системных данных, на данный момент, это базы данных SQL. Вот простейший пример хранения пользователей, групп и связей между ними. Необходимо всего три таблицы.
SQL CREATE statements: create table user ( id integer primary key, name char(64), pass char(64) ); create table group ( id integer primary key, name char(64) ); create table map ( user integer, group integer ); TABLE USER: Column | Type | Modifiers --------+---------------+----------- id | integer | not null name | character(64) | pass | character(64) | TABLE GROUP: Column | Type | Modifiers --------+---------------+----------- id | integer | not null name | character(64) | TABLE MAP: Column | Type | Modifiers --------+---------+----------- user | integer | group | integer |
Заполним таблички данными:
letme=# select id, name from user; id | name ----+------------------ 1 | Damian 2 | Clive 3 | Lana (3 rows) letme=# select * from group; id | name ----+------------------ 1 | Admin 2 | Users 3 | Moderators (3 rows) letme=# select * from map; user | group -----+----- 1 | 1 1 | 2 3 | 2 3 | 3 2 | 2 (4 rows)
В данном примере, пользователи связаны с такими группами:
Damian: Users, Admin Clive: Users Lana: Users, Moderators
Контроль во время выполнения
Приложение контролирует права доступа, после того как пользователем был произведен вход в систему. Поэтому, Вы можете скомбинировать контроль доступа с функцией входа в систему. Для примера: разрешить доступ для определенной группы пользователей в выходные дни. Если так, то проверка доступа производится только после успешного входа в систему, когда имя пользователя и пароль введены верно.
Наиболее простым методом применения прав доступа есть:
Вход в систему, проверка имени пользователя и пароля
Для неуспешного входа – запретить доступ, вывести сообщение об ошибке и т. д.
Для успешного входа, загрузить список прав доступа из базы данных для группы, в которую данный пользователь включен.
Все проверки на наличие прав доступа к функциям происходят позже.
Во время выполнения, хранилищем для групп пользователей может служить простой хэш. Он может быть как глобальным, так и включенным в объект сессии пользователя – в зависимости от дизайна системы. Для упрощения, в примерах используется глобальный хэш, но если Вы будете использовать этот код, запомните, что обязательно нужно очищать и создавать заново этот глобальный хэш, для каждого запроса пользователя, сразу же после входа в систему, или изменении пользовательской сессии. Можно также удалять все данные сессии пользователя после выхода из системы, но это можно рассматривать только как опцию, а не как единственно верный способ решения.
( В «правильных» системах пароли пользователей хранятся в захэшированном виде, но ето тема для отдельной статьи )
#!/usr/bin/perl use strict; use DBI; use Data::Dumper; our $USER_NAME; our $USER_ID; our %USER_GROUPS; my $DBH = DBI->connect( "dbi:Pg:dbname=letme", "postgres", "", { AutoCommit => 0 } ); # this is just an example! # username and password acquiring depends on the specific application user_login( 'Damian', 'secret4' ); print "User logged in: $USER_NAME\n"; print "User id: $USER_ID\n"; print "User groups: " . join( ', ', keys %USER_GROUPS ) . "\n"; sub user_login { my $user_name = shift; my $user_pass = shift; $USER_NAME = undef; $USER_ID = undef; %USER_GROUPS = (); # both name and password are required die "Empty user name" if $user_name eq ''; die "Empty user password" if $user_pass eq ''; eval { my $ar = $DBH->selectcol_arrayref( 'SELECT ID FROM USER WHERE NAME = ? AND PASS = ?', {}, $user_name, $user_pass ); $USER_ID = shift @$ar; die "Wrong user name or password" unless $USER_ID > 0; $USER_NAME = $user_name; # loading groups my $ar = $DBH->selectcol_arrayref( 'SELECT GROUP FROM MAP WHERE USER = ?', {}, $USER_ID ); %USER_GROUPS = map { $_ => 1 } @$ar; }; if( $@ ) { # something failed, it is important to clear user data here $USER_NAME = undef; $USER_ID = undef; %USER_GROUPS = (); # propagate error die $@; } }
Если пароль Damian правильный, данный код выведет:
User logged in: Damian User id: 1 User groups: 1, 2
Функция проверки группового доступа стала еще проще:
sub check_access { my $group = shift; return 0 unless $group > 0; return $USER_GROUPS{ $group }; }
Код, который проверяет права доступа после входа в систему, может быть таким:
sub edit_data { # require user to be in group 1 (admin) to edit data... die "Access denied" unless check_access( 1 ); # user allowed, group 1 check successful ... } or if( check_access( 1 ) ) { # user ok } else { # access denied }
Команды доступа
Следующая проблема – определение функций, которые доступны для групп. Если эта информация статичная ( в большинстве случаев это так), ее можна хранить в конфигурационных файлах.
LOGIN: 2 EDIT: 1
Команда EDIT доступна для группы 1 ( администраторы) а команда LOGIN – группе 2 ( все пользователи).
Другой пример – разрешить вход в систему на выходных только для администраторов.
# all users for mon-fri LOGIN_WEEKDAYS: 2 # only admin for sat-sun LOGIN_WEEKENDS: 1
Администраторы входят в обе группы (1, 2), таким образом, они получают доступ к системе в любое время. Все остальные пользователи на выходных доступа не имеют.
Этот список групп включает в себя группу модераторов. Полезно для выполнения модераторами своей роботы на выходных. Подразумевается использование оператора OR.
# only admin or moderators for sat-sun LOGIN_WEEKENDS: 1, 3
Такой набор групп называется политикой прав доступа.
Сейчас политика содержит в себе только один уровень, и только один оператор OR между группами. В реальности же, политики могут быть комплексными. Но нет причин переделывать их, каждая большая система может работать с несколькими уровнями, а также, использовать оператор AND.
LOGIN_WEEKENDS: 1+3, 4, 1+5+9
Такая политика разрешает доступ на выходных только для пользователей следующих групп:
1 AND 3 OR 4 OR 1 AND 5 AND 9
Функция входа должна проверять политику LOGIN_WEEKENDS до того, как позволять пользователю работать в системе. Поэтому Вам нужна функция для считывания политик прав доступа из конфигурационных файлов:
our %ACCESS_POLICY; sub read_access_config { my $fn = shift; # config file name open( my $f, $fn ); while( <$f> ) { chomp; next unless /\S/; # skip whitespace next if /^[;#]/; # skip comments die "Syntax error: $_\n" unless /^\s*(\S+?):\s*(.+)$/; my $n = uc $1; # policy name: LOGIN_WEEKENDS my $v = $2; # groups lsit: 1+3, 4, 1+5+9 # return list of lists: # outer list uses comma separator, inner lists use plus sign separator $ACCESS_POLICY{ $n } = access_policy_parse( $v ); } close( $f ); } sub access_policy_parse { my $policy = shift; return [ map { [ split /[\s\+]+/ ] } split /[\s,]+/, $policy ]; }
Для политики LOGIN_WEEKENDS, результатом в %ACCESS_POLICY будет:
$ACCESS_POLICY{ 'LOGIN_WEEKENDS' } => [ [ '1', '3' ], [ '4' ], [ '1', '5', '9' ] ];
Чтобы соответствовать данной политике, пользователь должен состоять в группах, перечисленных в любом из внутренних списков :
sub check_policy { my $policy = shift; my $out_arr = $ACCESS_POLICY{ $policy }; die "Invalid policy name; $policy\n" unless $out_arr; return check_policy_tree( $out_arr ); } sub check_policy_tree { my $out_arr = shift; for my $in_arr ( @$out_arr ) { my $c = 0; # matching groups count for my $group ( @$in_arr ) { $c++ if $USER_GROUPS{ $group }; } # matching groups is equal to all groups count in this list # policy match! return 1 if $c == @$in_arr; } # if this code is reached then policy didn't match return 0; }
Пример:
sub user_login { # login checks here ... # login ok, check weekday policy my $wday = (localtime())[6]; my $policy; if( $wday == 0 or $wday == 6 ) { $policy = 'LOGIN_WEEKEND'; } else { $policy = 'LOGIN_WEEKDAY'; } die "Login denied" unless check_policy( $policy ); } sub edit_data { # require user to be in group 1 (admin) to edit data... die "Access denied" unless check_policy( 'EDIT' ); # user allowed, 'EDIT' policy match ... }
Теперь у Вас есть все части схемы контроля доступа:
Конфигурация синтаксиса политик
Парсер политик
Хранилище групп пользователей и связей между ними
Загрузчик групп пользователей
Функция проверки политик
Разделение информации
В многопользовательской системе всегда есть способ разделения информации, сохраненной в базе данных. Это значит, что каждый пользователь должен видеть только ту часть информации, владельцем которой есть группа, в которую пользователь включен.
Решение этой проблемы никак не связано с политиками прав доступа. Каждая запись должна включать одно или несколько полей, заполненных группами, которые имеют доступ к этим данным. Все SQL запросы для чтения информации также должны учитывать эти поля:
my $rg = join ',', grep { $USER_GROUPS{ $_ } } keys %USER_GROUPS; my $ug = join ',', grep { $USER_GROUPS{ $_ } } keys %USER_GROUPS; my $sql = "SELECT * FROM TABLE_NAME WHERE READ_GROUP IN ( $rg ) AND UPDATE_GROUP IN ( $ug )";
Результат будет включать только те записи, доступ на чтение и редактирование к которым разрешен для группы, в которой состоит пользователь. Иногда есть необходимость вывести все записи с определенной группой чтения, несмотря на то, что пользователь не имеет права изменять эти записи. В таком случае нужно использовать только поле READ_GROUP для выборки, и останавливать действия пользователей, которые пытаются изменить запись, не имея соответствующих прав:
my $rg = join ',', grep { $USER_GROUPS{ $_ } } keys %USER_GROUPS; my $sql = "SELECT * FROM TABLE_NAME WHERE READ_GROUP IN ( $rg )"; $sth = $dbh->prepare( $sql ); $sth->execute(); $hr = $sth->fetchrow_hashref(); die "Edit access denied" unless check_access( $hr->{ 'UPDATE_GROUP' } );
Когда проверка доступа производится непосредственно после вызова SELECT, можно хранить полные строки политик доступа внутри CHAR полей:
$hr = $sth->fetchrow_hashref(); die "Edit access denied" unless check_policy_record( $hr, 'UPDATE_GROUP' ); sub check_policy_record { my $hr = shift; # hash with record data my $field = shift; # field containing policy string my $policy = $hr->{ $field }; my $tree = access_policy_parse( $policy ); return check_policy_tree( $tree ); }
Заключение
Описанная схема контроля доступа проста и удобна в использовании. Она не покрывает всех возможных случаев, но каждое приложение имеет свои уникальные потребности. В некоторых случаях, контроль доступа можно проводить и на нижних уровнях, например на уровне базы данных. Удачи в построении Вашей собственной системы безопасности!
Статья является переводом Elements of Access Control и публикуется с любезного разрешения автора Vladi Belperchinov-Shabanski
