Forráskód Böngészése

Admin acl working!

George S. Baugh 5 éve
szülő
commit
6a0f18594e

+ 32 - 8
lib/Trog/Auth.pm

@@ -22,21 +22,38 @@ Throws exceptions in the event the session database cannot be accessed.
 
 =head1 FUNCTIONS
 
-=head2 session2user(sessid) = STRING
+=head2 session2user(sessid) = (STRING, INT)
 
-Translate a session UUID into a username.
+Translate a session UUID into a username and id.
 
-Returns empty string on no active session.
+Returns empty strings on no active session.
 
 =cut
 
 sub session2user ($sessid) {
     my $dbh = _dbh();
-    my $rows = $dbh->selectall_arrayref("SELECT name FROM sess_user WHERE session=?",{ Slice => {} }, $sessid);
-    return '' unless ref $rows eq 'ARRAY' && @$rows;
-    return $rows->[0]->{name};
+    my $rows = $dbh->selectall_arrayref("SELECT name,id FROM sess_user WHERE session=?",{ Slice => {} }, $sessid);
+    return ('','') unless ref $rows eq 'ARRAY' && @$rows;
+    return ($rows->[0]->{name},$rows->[0]->{id});
 }
 
+=head2 acls4user(user_id) = ARRAYREF
+
+Return the list of ACLs belonging to the user.
+The function of ACLs are to allow you to access content tagged 'private' which are also tagged with the ACL name.
+
+The 'admin' ACL is the only special one, as it allows for authoring posts, configuring tCMS, adding series (ACLs) and more.
+
+=cut
+
+sub acls4user($user_id) {
+    my $dbh = _dbh();
+    my $records = $dbh->selectall_arrayref("SELECT acl FROM user_acl WHERE user_id = ?", { Slice => {} }, $user_id);
+    return () unless ref $records eq 'ARRAY' && @$records;
+    my @acls = map { $_->{acl} } @$records;
+    return \@acls;
+ }
+
 =head2 mksession(user, pass) = STRING
 
 Create a session for the user and waste all other sessions.
@@ -67,11 +84,18 @@ Returns True or False (likely false when user already exists).
 
 =cut
 
-sub useradd ($user, $pass) {
+sub useradd ($user, $pass, $acls) {
     my $dbh = _dbh();
     my $salt = create_uuid();
     my $hash = sha256($pass.$salt);
-    return $dbh->do("INSERT INTO user (name,salt,hash) VALUES (?,?,?)", undef, $user, $salt, $hash);
+    my $res =  $dbh->do("INSERT INTO user (name,salt,hash) VALUES (?,?,?)", undef, $user, $salt, $hash);
+    return unless $res && ref $acls eq 'ARRAY';
+
+    #XXX this is clearly not normalized with an ACL mapping table, will be an issue with large number of users
+    foreach my $acl (@$acls) {
+        return unless $dbh->do("INSERT INTO user_acl (user_id,acl) VALUES ((SELECT id FROM user WHERE name=?),?)", undef, $user, $acl);
+    }
+    return 1;
 }
 
 my $dbh;

+ 13 - 0
lib/Trog/Data/DUMMY.pm

@@ -121,6 +121,19 @@ my $example_posts = [
         version      => 0,
         preview      => '/img/sys/testpattern.jpg',
     },
+    {
+        content_type => 'text/plain',
+        data         => "Admin ACL",
+        href         => "/config",
+        local_href   => '/config',
+        title        => 'admin',
+        user         => 'Nobody',
+        id           => "900",
+        tags         => ['acl'],
+        created      => time(),
+        version      => 0,
+        preview      => '/img/sys/testpattern.jpg',
+    }
 ];
 
 =head1 CONSTRUCTOR

+ 27 - 4
lib/Trog/Routes/HTML.pm

@@ -4,7 +4,7 @@ use strict;
 use warnings;
 
 no warnings 'experimental';
-use feature qw{signatures};
+use feature qw{signatures state};
 
 use File::Touch();
 
@@ -92,6 +92,14 @@ our %routes = (
         method   => 'GET',
         callback => \&Trog::Routes::HTML::series,
     },
+    '/config/series' => {
+        method => 'GET',
+        callback => \&Trog::Routes::HTML::series_edit,
+    },
+    '/config/series/save' => {
+        method   => 'POST',
+        callback => \&Trog::Routes::HTML::series_edit,
+    },
     '/sitemap', => {
         method   => 'GET',
         callback => \&Trog::Routes::HTML::sitemap,
@@ -183,6 +191,7 @@ sub _generic_route ($rname, $code, $title, $query,$input,$render_cb) {
     my $content = $processor->render("$rname.tx", {
         title    => $title,
         route    => $query->{route},
+        user     => $query->{user},
         styles   => $styles,
     });
     return Trog::Routes::HTML::index($query, $input, $render_cb, $content, $styles);
@@ -247,7 +256,7 @@ sub login ($query, $input, $render_cb) {
     if ($postdata->{username} && $postdata->{password}) {
         if (!$hasusers) {
             # Make the first user
-            Trog::Auth::useradd($postdata->{username}, $postdata->{password});
+            Trog::Auth::useradd($postdata->{username}, $postdata->{password}, ['admin'] );
             File::Touch::touch("$ENV{HOME}/.tcms/has_users");
         }
 
@@ -284,6 +293,8 @@ sub config ($query, $input, $render_cb) {
         $query->{to} = '/config';
         return login($query,$input,$render_cb);
     }
+    return forbidden($query, $input, $render_cb) unless grep { $_ eq 'admin' } @{$query->{acls}};
+
     my $tags = ['profile'];
     my $posts = _post_helper($query, $tags);
     my $css   = _build_themed_styles('config.css');
@@ -304,6 +315,7 @@ sub config ($query, $input, $render_cb) {
         route       => '/about',
         category    => '/about',
         types       => ['profile'],
+        acls        => _post_helper({}, ['acl']),
         can_edit    => 1,
         posts       => $posts,
         edittype    => 'profile',
@@ -385,9 +397,10 @@ Display the route for making new posts.
 
 sub post ($query, $input, $render_cb) {
     if (!$query->{user}) {
-        $query->{to} = '/config';
+        $query->{to} = '/post';
         return login($query,$input,$render_cb);
     }
+    return forbidden($query, $input, $render_cb) unless grep { $_ eq 'admin' } @{$query->{acls}};
 
     my $tags  = _coerce_array($query->{tag});
     my $posts = _post_helper($query, $tags);
@@ -453,7 +466,7 @@ sub posts ($query, $input, $render_cb) {
 }
 
 sub _post_helper ($query, $tags) {
-    my $data = Trog::Data->new($conf);
+    state $data = Trog::Data->new($conf);
     return $data->get(
         page  => int($query->{page} || 1),
         limit => int($query->{limit} || 25),
@@ -468,6 +481,16 @@ sub series ($query, $input, $render_cb) {
     return Trog::Routes::HTML::index($query,$input,$render_cb);
 }
 
+sub series_edit ($query, $input, $render_cb) {
+    if (!$query->{user}) {
+        $query->{to} = '/series/edit';
+        return login($query,$input,$render_cb);
+    }
+    return forbidden($query, $input, $render_cb) unless grep { $_ eq 'admin' } @{$query->{acls}};
+
+    return Trog::Routes::HTML::index($query,$input,$render_cb);
+}
+
 =head2 sitemap
 
 Return the sitemap index unless the static or a set of dynamic routes is requested.

+ 6 - 1
schema/auth.schema

@@ -12,4 +12,9 @@ CREATE TABLE IF NOT EXISTS session (
 
 CREATE INDEX IF NOT EXISTS username_idx ON user(name);
 
-CREATE VIEW IF NOT EXISTS sess_user AS SELECT user.name AS name, session.id AS session FROM user JOIN session ON session.user_id=user.id;
+CREATE VIEW IF NOT EXISTS sess_user AS SELECT user.name AS name, user.id AS id, session.id AS session FROM user JOIN session ON session.user_id=user.id;
+
+CREATE TABLE IF NOT EXISTS user_acl (
+    user_id INTEGER NOT NULL UNIQUE REFERENCES user(id) ON DELETE CASCADE,
+    acl TEXT NOT NULL
+);

+ 15 - 2
www/server.psgi

@@ -48,6 +48,16 @@ my %cache_control = (
     static     => "$cc: public, max-age=604800, immutable",
 );
 
+=head2 $app
+
+Dispatches requests based on %routes built above.
+
+The dispatcher here does *not* do anything with the authn/authz data.  It sets those in the 'user' and 'acls' parameters of the query object passed to routes.
+
+If a path passed is not a defined route (or regex route), but exists as a file under www/, it will be served up immediately.
+
+=cut
+
 my $app = sub {
     my $env = shift;
 
@@ -68,10 +78,13 @@ my $app = sub {
         $cookies = CGI::Cookie->parse($env->{HTTP_COOKIE});
     }
 
-    my $active_user = '';
+    my ($active_user,$user_id) = ('','');
     if (exists $cookies->{tcmslogin}) {
-         $active_user = Trog::Auth::session2user($cookies->{tcmslogin}->value);
+         ($active_user,$user_id) = Trog::Auth::session2user($cookies->{tcmslogin}->value);
     }
+
+    $query->{acls} = Trog::Auth::acls4user($user_id) // [] if $user_id;
+
     $query->{user}   = $active_user;
     $query->{domain} = $env->{HTTP_HOST};
     $query->{route}  = $path;

+ 5 - 1
www/styles/screen.css

@@ -164,12 +164,16 @@ button#clickme:active {
  background-color: #333;
  width: 100%;
 }
-.Submissions input, .Submissions textarea {
+.Submissions input, .Submissions textarea, .Submissions select {
  width: 95%;
  display: block;
  margin-right: auto;
  margin-left: auto;
 }
+
+.Config input, .Config textarea, .Config select {
+ display:inline;
+}
 .Submissions textarea {
  height: 20em;
  vertical-align: top;

+ 5 - 5
www/templates/config.tx

@@ -7,7 +7,7 @@ This controls your Theme and Data Model used.
 The Data Model *must* be 0-configuration.
 If for example, you use mysql it will have to rely on either a local server, valid config file or connection proxy/pooler locally.
 <hr />
-<form id="mainConfig" method="post" action="/config/save">
+<form class="Submissions Config" id="mainConfig" method="post" action="/config/save">
     Theme:
     <select class="cooltext" name="theme">
         <option value="" <: if ( $current_theme == '' ) { :>selected<: } :> >default</option>
@@ -15,14 +15,15 @@ If for example, you use mysql it will have to rely on either a local server, val
         <option value="<: $theme :>" <: if ( $current_theme == $theme ) { :>selected<: } :> ><: $theme :></option>
         : }
     </select>
-    <br />
+    <div>
     Data Model:
     <select class="cooltext" name="data_model">
         : for $data_models -> $dm {
         <option value="<: $dm :>"><: $dm :></option>
         : }
     </select>
-    <br /><br />
+    </div>
+    <br />
     <input type="submit" class="coolbutton" value="Commit Changes" />
 </form>
 <hr />
@@ -42,14 +43,13 @@ Want to write your own theme?
 Clone a theme here then see the <a href="https://tcms.troglodyne.net/index.php?nav=5&post=fileshare/manual/Chapter 03-Customization.post" title="GET UR MIND RITE">styling guide</a>
 for information on how tCMS' templates, image sets and CSS work in the theming system.
 <hr />
-<form id="themeCloner" method="post" action="/themeclone">
+<form class="Submissions" id="themeCloner" method="post" action="/themeclone">
     Theme:
     <select class="cooltext" name="theme">
         : for $themes -> $theme {
         <option value="<: $theme :>"><: $theme :></option>
         : }
     </select>
-    <br />
     <input type="text" class="cooltext" placeholder="newTheme" name="newtheme" />
     <input type="submit" class="coolbutton" value="Clone" />
 </form>

+ 3 - 1
www/templates/forbidden.tx

@@ -1,3 +1,5 @@
 403 Forbidden
 <br /><br />
-Log in <a href="/login?to=<: $route :>">here</a>.
+: if ( !$user ) {
+    Log in <a href="/login?to=<: $route :>">here</a>.
+: }

+ 7 - 1
www/templates/profile.tx

@@ -4,12 +4,18 @@
     Avatar *<br /><input class="cooltext" type="file" name="file" />
     Wallpaper<br /><input type="file" class="cooltext" name="wallpaper" placeholder="PROMO.JPG"></input>
     Title  <br /><input class="cooltext" type="text" name="title" />
-    Visibility<br />
+    Profile Visibility<br />
     <select class="cooltext" name="visibility">
         : for $post_visibilities -> $visibility {
             <option value="<: $visibility :>"><: $visibility :></option>
         : }
     </select>
+    ACLs / Series<br/ >
+    <select multiple class="cooltext" name="acls">
+        : for $acls -> $acl {
+            <option value="<: $acl.title :>"><: $acl.data :></option>
+        : }
+    </select>
     Content<br /><textarea class="cooltext" name="comment" placeholder="Potzrebie"></textarea>
     <input type="hidden" name="app" value="file" />
     <input class="coolbutton" type="submit" value="Publish" text="Publish" />

+ 1 - 0
www/templates/sysbar.tx

@@ -3,6 +3,7 @@
     <span id="configbar">
         <a class="topbar" title="Back home" href="/">Home</a>
         <a class="topbar" title="Edit Various Settings" href="/config">Settings</a>
+        <a class="topbar" title="Series CRUD" href="/config/series">Series</a>
         <a class="topbar" title="Compose Posts" href="/post">Post</a>
     </span>
 </div>