Snaha o zprovoznění ACL

Upozornění: Tohle vlákno je hodně staré a informace nemusí být platné pro současné Nette.
Ondřej Mirtes
Člen | 1536
+
0
-

Ahoj,
přihlašování a autorizaci uživatelů v Nette potřebuji až nyní a rozhodl jsem se pro dynamickou správu rolí a zdrojů. A $user->isAllowed() mi zkrátka nevrací to, co bych očekával.

Pro jistotu jsem postnu kód, do kterého jsem mírně sahal:

<?php

class Acl extends Permission
{

    const ACL_TABLE = 'users_acl';
    const PRIVILEGES_TABLE = 'users_privileges';
    const RESOURCES_TABLE = 'users_resources';
    const ROLES_TABLE = 'users_roles';

    /**
     * Access Control List constructor
     */
    public function __construct()
    {
        $this->init();
    }

    /**
     * Builds ACL rules list from database
     */
    private function init()
    {
        $roles = dibi::fetchAll('
            SELECT r.name AS role_name, rp.name AS parent_role
            FROM [::' . self::ROLES_TABLE . '] r LEFT JOIN [::' . self::ROLES_TABLE . '] rp ON r.parent_id = rp.id
            ORDER BY r.parent_id ASC;');

        foreach ($roles as $r) {
            if ($r['parent_role'] !== NULL) {
                $parents = $this->getRoleParents($r['parent_role']);
                $parents[] = $r['parent_role'];
            } else $parents = $r['parent_role']; // or $parents = NULL

            $this->addRole($r['role_name'], $parents);
        }

        $resources = dibi::query('SELECT name AS resource FROM [::' . self::RESOURCES_TABLE . '] ORDER BY id ASC;');
        $resources = $resources->fetchAll();

        foreach ($resources as $r) $this->addResource($r['resource']);

        $rules = dibi::fetchAll('
            (SELECT  ro.name AS role,
                     pr.name AS privilege,
                     re.name AS resource,
                     a.allowed AS allowed
             FROM [::' . self::ACL_TABLE . '] a
                 JOIN [::' . self::ROLES_TABLE . '] ro ON a.role_id = ro.id
                 JOIN [::' . self::PRIVILEGES_TABLE . '] pr ON a.privilege_id = pr.id
                 JOIN [::' . self::RESOURCES_TABLE . '] re ON a.resource_id = re.id
             ORDER BY ro.id ASC)
            UNION
            (SELECT  ro.name AS role,
                     NULL AS privilege,
                     NULL AS resource,
                     a.allowed AS allowed
             FROM [::' . self::ACL_TABLE . '] a
                 JOIN [::' . self::ROLES_TABLE . '] ro ON a.role_id = ro.id
             WHERE
                 a.privilege_id IS NULL
                 AND
                 a.resource_id IS NULL
             ORDER BY ro.id ASC);'
        );

        foreach ($rules as $r) {
            // NOTE: allowed column can be nullable, because NULL means 'all'
            if ($r['allowed'] == '1') $this->allow($r['role'], $r['resource'], $r['privilege']);
            elseif ($r['allowed'] == '0') $this->deny($r['role'], $r['resource'], $r['privilege']);
            // administrator has allowed all [::' . self::PRIVILEGES_TABLE . '] to all [::' . self::RESOURCES_TABLE . ']
            elseif ($r['privilege'] === NULL && $r['resource'] === NULL) $this->allow($r['role']);
        }
    }
}

Config.ini:

service.Nette-Security-IAuthorizator = Acl
service.Nette-Security-IAuthenticator = Authenticator
<?php

class Authenticator extends Object implements IAuthenticator
{
    const USERS_TABLE = 'users';

    public function authenticate(array $credentials)
    {
        $username = $credentials[self::USERNAME];
        $password = MyTools::passwordEncode($credentials[self::PASSWORD], $credentials[self::USERNAME]);

        $row = dibi::fetch('SELECT u.name, u.password, r.name AS role
                            FROM [::' . self::USERS_TABLE . '] u
                            JOIN [::' . Acl::ROLES_TABLE . '] r ON (u.role_id = r.id)
                            WHERE u.login=%s', $username, ' AND u.is_deleted=0
                            ORDER BY u.id DESC LIMIT 0,1');

        //user not found
        if ($row == null) throw new AuthenticationException('User ' . $username . ' not found.', self::IDENTITY_NOT_FOUND);

        //passwords don't match
        if ($row->password !== $password) throw new AuthenticationException('Invalid password.', self::INVALID_CREDENTIAL);

        return new Identity($row->name, $row->role);
    }

}

V databázových tabulkách mám tato data:

CREATE TABLE `users` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `role_id` bigint(20) NOT NULL,
  `name` tinytext COLLATE utf8_czech_ci NOT NULL,
  `email` tinytext COLLATE utf8_czech_ci NOT NULL,
  `login` tinytext COLLATE utf8_czech_ci NOT NULL,
  `password` tinytext COLLATE utf8_czech_ci NOT NULL,
  `is_deleted` tinyint(1) NOT NULL DEFAULT '0',
  PRIMARY KEY (`id`)
) ENGINE=MyISAM AUTO_INCREMENT=2 DEFAULT CHARSET=utf8 COLLATE=utf8_czech_ci;

INSERT INTO `users` VALUES ('1', '2', 'Ondřej Mirtes', 'ondrej@mirtes.cz', 'lasthunter', '***', '0');

CREATE TABLE `users_acl` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `role_id` bigint(20) NOT NULL,
  `privilege_id` bigint(20) DEFAULT NULL,
  `resource_id` bigint(20) DEFAULT NULL,
  `allowed` tinyint(1) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=MyISAM AUTO_INCREMENT=2 DEFAULT CHARSET=utf8 COLLATE=utf8_czech_ci;

INSERT INTO `users_acl` VALUES ('1', '1', '1', NULL, '1');

CREATE TABLE `users_privileges` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `name` tinytext COLLATE utf8_czech_ci NOT NULL,
  PRIMARY KEY (`id`)
) ENGINE=MyISAM AUTO_INCREMENT=2 DEFAULT CHARSET=utf8 COLLATE=utf8_czech_ci;

INSERT INTO `users_privileges` VALUES ('1', 'default');

CREATE TABLE `users_resources` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `name` tinytext COLLATE utf8_czech_ci NOT NULL,
  PRIMARY KEY (`id`)
) ENGINE=MyISAM AUTO_INCREMENT=2 DEFAULT CHARSET=utf8 COLLATE=utf8_czech_ci;

INSERT INTO `users_resources` VALUES ('1', 'Default');

CREATE TABLE `users_roles` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `parent_id` bigint(20) DEFAULT NULL,
  `name` tinytext COLLATE utf8_czech_ci NOT NULL,
  PRIMARY KEY (`id`)
) ENGINE=MyISAM AUTO_INCREMENT=3 DEFAULT CHARSET=utf8 COLLATE=utf8_czech_ci;

INSERT INTO `users_roles` VALUES ('1', NULL, 'guest'), ('2', '1', 'registered');

A v BasePresenteru v rámci testování mám ve startupu tento kód:

$user = Environment::getUser();
$user->authenticate('lasthunter','***');
$user->signOut();
Debug::dump($user->isAllowed('Default'));

I když signOut zakomentuji a i když ne (user má pak střídavě roli guest a registered), tak mi dump vyhazuje stále false.

Kde mám chybu? :) Je toho hodně k analyzování, tak se s ním snad někdo poperete :) Díky moc.

Editoval LastHunter (23. 6. 2009 14:03)

vlki
Člen | 218
+
0
-

Takže… Před pár dny jsem se tím bavil, tak řeknu, co si myslím:)

Problém bude asi v tom, že při výběru práv se vezmou buď ty, které mají resource i privilege (viz ten inner join v první části unionu) nebo ty, které zaráz nemají resource i privilege (druhá část unionu).

$rules = dibi::fetchAll('
    (SELECT  ro.name AS role,
             pr.name AS privilege,
             re.name AS resource,
             a.allowed AS allowed
     FROM [::' . self::ACL_TABLE . '] a
         JOIN [::' . self::ROLES_TABLE . '] ro ON a.role_id = ro.id
         JOIN [::' . self::PRIVILEGES_TABLE . '] pr ON a.privilege_id = pr.id
         JOIN [::' . self::RESOURCES_TABLE . '] re ON a.resource_id = re.id
     ORDER BY ro.id ASC)
    UNION
    (SELECT  ro.name AS role,
             NULL AS privilege,
             NULL AS resource,
             a.allowed AS allowed
     FROM [::' . self::ACL_TABLE . '] a
         JOIN [::' . self::ROLES_TABLE . '] ro ON a.role_id = ro.id
     WHERE
         a.privilege_id IS NULL
         AND
         a.resource_id IS NULL
     ORDER BY ro.id ASC);'
);

Pokud do acl pak vložíš

INSERT INTO `users_acl` VALUES ('1', '1', '1', NULL, '1');

proti tabulce

CREATE TABLE `users_acl` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `role_id` bigint(20) NOT NULL,
  `privilege_id` bigint(20) DEFAULT NULL,
  `resource_id` bigint(20) DEFAULT NULL,
  `allowed` tinyint(1) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=MyISAM AUTO_INCREMENT=2 DEFAULT CHARSET=utf8 COLLATE=utf8_czech_ci;

tak nedefunuješ resource, ale zato máš definovanou privilege. Ve výše zmíněném výběru pravidel se ti tak tvé pravidlo neobjeví.

Pokud pravidlo změníš na

INSERT INTO `users_acl` VALUES ('1', '1', '1', '1', '1');

mohlo by to jít:)

Btw. v takových případech se hodí dump Acl objektu po vygenerování. Uvidíme, jaká pravidla tam opravdu jsou…

Ondřej Mirtes
Člen | 1536
+
0
-

Tak jsem to upravil na samé jedničky (viz níže) a dump ACL je takový:

Acl Object
(
    [roles:private] => Array
        (
            [guest] => Array
                (
                    [parents] => Array
                        (
                        )

                    [children] => Array
                        (
                            [registered] => 1
                        )

                )

            [registered] => Array
                (
                    [parents] => Array
                        (
                            [guest] => 1
                        )

                    [children] => Array
                        (
                        )

                )

        )

    [resources:private] => Array
        (
            [Default] => Array
                (
                    [parent] =>
                    [children] => Array
                        (
                        )

                )

        )

    [rules:private] => Array
        (
            [allResources] => Array
                (
                    [allRoles] => Array
                        (
                            [allPrivileges] => Array
                                (
                                    [type] =>
                                    [assert] =>
                                )

                            [byPrivilege] => Array
                                (
                                )

                        )

                    [byRole] => Array
                        (
                        )

                )

            [byResource] => Array
                (
                    [Default] => Array
                        (
                            [byRole] => Array
                                (
                                    [guest] => Array
                                        (
                                            [byPrivilege] => Array
                                                (
                                                    [default] => Array
                                                        (
                                                            [type] => 1
                                                            [assert] =>
                                                        )

                                                )

                                        )

                                )

                        )

                )

        )

    [queriedRole:private] =>
    [queriedResource:private] =>
)

Sorry, že jsem nepoužil laděnčin dump, místo vnořenějších polí mi ukazovala jen tři tečky :)

Tak k věci – ten (myslím že Romana Sklenáře) SQL kód třídy ACL jsem moc nezkoumal, protože je tam na mě moc JOINů a UNIONů, takže jsem z toho nevyčetl, co přesně a v jaké situaci to dělá. Měl jsem ale zato, že když někde použiju NULL, tak se to bere jako „pro vše“. Jak bych tedy v databázi měl definovat, že guest má přístup k default privilege všude? V dokumentaci a „ručním“ volání allow/deny se to zmiňuje a podle mě to jde.

Chci totiž použít pravidla, že resource = presenter a privilege = action, ať je to sjednocené a přehledné.

Jsem v ACL nováček, možná proto plácám blbosti. Je v tom na mě moc nových pojmů a musím se hodně snažit, abych je správně používal :)

romansklenar
Člen | 655
+
0
-

LastHunter napsal(a):

Sorry, že jsem nepoužil laděnčin dump, místo vnořenějších polí mi ukazovala jen tři tečky :)

Debug::$maxDepth = 10;  // hloubka zanoření polí
Debug::$maxLen   = 999; // maximální délka řetězce

Jinak k tomu kódu: je to psané dost dávno a očividně je tam pár věcí, na které se nemyslelo nebo byly kvůli jednoduchosti vynechány, už nevím. Každopádně dneska bych to celé přepsal do modelu, cpát SQL do Acl není moc hezké.

Ondřej Mirtes
Člen | 1536
+
0
-

Díky za radu o vnoření pole :)

Tak jsem se hecnul a napsal si vlastní implementaci tvého řešení (samozřejmě tebou hodně inspirovanou) a bez UNIONů, myslím tam i na aplikování na všechna privileges i všechny resources (resource_id a privilege_id mohou být NULL), SQL dotazy jsem oddělil do modelu. Tradadá, tu to je (otestování a funkční, resp. funguje to tak, jak očekávám :)):

<?php

class Acl extends Permission {

    public function __construct() {
        $model = new AclModel();

        foreach($model->getRoles() as $role)
            $this->addRole($role->name, $role->parent_name);

        foreach($model->getResources() as $resource)
            $this->addResource($resource->name);

        foreach($model->getRules() as $rule)
            $this->{$rule->allowed == 1 ? 'allow' : 'deny'}($rule->role, $rule->resource, $rule->privilege);
    }

}
<?php

class AclModel extends BaseModel {

    const ACL_TABLE = 'users_acl';
    const PRIVILEGES_TABLE = 'users_privileges';
    const RESOURCES_TABLE = 'users_resources';
    const ROLES_TABLE = 'users_roles';

    public function getRoles() {
        return dibi::fetchAll('SELECT r1.name, r2.name as parent_name
                               FROM [::'. self::ROLES_TABLE . '] r1
                               LEFT JOIN [::'. self::ROLES_TABLE . '] r2 ON (r1.parent_id = r2.id)
                              ');
    }

    public function getResources() {
        return dibi::fetchAll('SELECT name FROM [::'. self::RESOURCES_TABLE . '] ');
    }

    public function getRules() {
        return dibi::fetchAll('
            SELECT
                a.allowed as allowed,
                ro.name as role,
                re.name as resource,
                p.name as privilege
                FROM [::' . self::ACL_TABLE . '] a
                JOIN [::' . self::ROLES_TABLE . '] ro ON (a.role_id = ro.id)
                LEFT JOIN [::' . self::RESOURCES_TABLE . '] re ON (a.resource_id = re.id)
                LEFT JOIN [::' . self::PRIVILEGES_TABLE . '] p ON (a.privilege_id = p.id)
        ');
    }

}

Editoval LastHunter (23. 6. 2009 22:54)

dotTwelve
Člen | 167
+
0
-

Reseni podle LustHuntera mi vyhazuje chybu:

Missing substitution for '' expression.

v dibi.php na radku: 668

insider
Člen | 31
+
0
-

dotTwelve: je to tim, ze pro dibi nemas nastavenou substituci, takze bud smaz vsude dvojtecky [:: prepis na [, nebo pokud mas prefixovane tabulky, muzes si do dibi pridat substituci

dotTwelve
Člen | 167
+
0
-

insider napsal(a):

dotTwelve: je to tim, ze pro dibi nemas nastavenou substituci, takze bud smaz vsude dvojtecky [:: prepis na [, nebo pokud mas prefixovane tabulky, muzes si do dibi pridat substituci

ok, tak jsem smazal :: a definuju konstanty tabulek napevno…
Diky

Editoval dotTwelve (30. 6. 2009 14:17)