
package Apache::Authn::Redmine2;

use strict;
use warnings FATAL => 'all', NONFATAL => 'redefine';

use DBI;
use Digest::SHA1;
use Authen::Simple::LDAP;
use Apache2::Module;
use Apache2::Access;
use Apache2::ServerRec qw();
use Apache2::RequestRec qw();
use Apache2::RequestUtil qw();
use Apache2::Const qw(:common :override :cmd_how);
use APR::Pool ();
use APR::Table ();

my %READ_ONLY_METHODS = map { $_ => 1 } qw/GET PROPFIND REPORT OPTIONS/;
my $DSN = "DBI:mysql:database=<enter database name>;host=localhost";
my $DB_USER = "<enter database username>";
my $DB_PASS = "<enter database password>";
my %AUTH_CACHE = ();

sub access_handler {
  return OK;
}

sub authen_handler {

  my $r = shift;

  unless ($r->some_auth_required) {
    $r->log_reason("No authentication has been configured");
    return FORBIDDEN;
  }

  my $project_id = get_project_identifier($r);
  my $is_read_only = is_read_only($r);
  
  my ($res, $redmine_pass) = $r->get_basic_auth_pw();
  my $redmine_user = $r->user;

  return $res unless $res == OK;

  return OK unless $project_id;
  return OK if is_cached($redmine_user, $redmine_pass, $project_id, $is_read_only);

  my $dbh = connect_database();

  my $is_public = is_public_project($dbh, $project_id);
  my $is_anonymous = is_anonymous($dbh);

  if ($is_public && $is_anonymous && $is_read_only) {
    $dbh->disconnect();
    cache($redmine_user, $redmine_pass, $project_id, $is_read_only);
    return OK; 
  }
  
  my $valid_credentials = are_valid_credentials($dbh, $redmine_user, $redmine_pass);

  unless ($valid_credentials) {
    $r->note_auth_failure();
    $r->log_reason("Invalid Credentials");
    $dbh->disconnect();
    return AUTH_REQUIRED;
  }

  if ($is_public && $is_read_only) {
    $dbh->disconnect();
    cache($redmine_user, $redmine_pass, $project_id, $is_read_only);
    return OK;
  }

  my $permissions = get_permissions($dbh, $redmine_user, $project_id);
  my $can_read = can_read($permissions);
  my $can_write = can_write($permissions);

  $dbh->disconnect();

  if ($is_read_only && $can_read || $can_write) {
    cache($redmine_user, $redmine_pass, $project_id, $is_read_only);
    return OK;
  }

  $r->log_reason("Forbidden request : " . $project_id . ", " . $is_read_only);
  return FORBIDDEN;
}

sub get_project_identifier {
  my $r = shift;
  
  my $location = $r->location;
  my ($identifier) = $r->uri =~ m{$location/*([^/]+)};

  return $identifier;
}

sub is_anonymous {
  my $dbh = shift;

  my $sth = $dbh->prepare(
    "SELECT value FROM settings WHERE settings.name='login_required';" 
  );

  $sth->execute();
  my @ret = $sth->fetchrow_array();
  $sth->finish();

  return $ret[0] == 0 ? 1 : 0;
}

sub is_read_only {
  my $r = shift;
  return defined $READ_ONLY_METHODS{$r->method};
}

sub is_public_project {
  my $dbh = shift;
  my $project_id = shift;

  my $sth = $dbh->prepare(
    "SELECT * FROM projects WHERE projects.identifier=? and projects.is_public=true;"
  );

  $sth->execute($project_id);
  my $ret = $sth->fetchrow_array ? 1 : 0;
  $sth->finish();

  return $ret;
}

sub are_valid_credentials {
  my $dbh = shift;
  my $redmine_user = shift;
  my $redmine_pass = shift;

  return 0 unless $redmine_user;

  my $pass_digest = Digest::SHA1::sha1_hex($redmine_pass);

  my $sth = $dbh->prepare(
    "SELECT hashed_password, auth_source_id FROM users WHERE users.login=? and users.status=1;"
  );

  $sth->execute($redmine_user);
  my ($hashed_password, $auth_source_id) = $sth->fetchrow_array;
  $sth->finish();

  return ldap_authenticate($dbh, $auth_source_id, $redmine_user, $redmine_pass) if $auth_source_id;

  return 0 unless $hashed_password;

  return ($pass_digest eq $hashed_password) ? 1 : 0;
}

sub ldap_authenticate {
  my $dbh = shift;
  my $auth_source_id = shift;
  my $username = shift;
  my $password = shift;

  my $sth = $dbh->prepare(
    "SELECT host,port,tls,account,account_password,base_dn,attr_login from auth_sources WHERE id = ?;"
  );

  $sth->execute($auth_source_id);
  my @rowldap = $sth->fetchrow_array;
  $sth->finish();
  
  my $ldap = Authen::Simple::LDAP->new(
    host => ($rowldap[2] == 1 || $rowldap[2] eq "t") ? "ldaps://$rowldap[0]" : $rowldap[0],
    port => $rowldap[1],
    basedn => $rowldap[5],
    binddn => $rowldap[3] ? $rowldap[3] : "",
    bindpw => $rowldap[4] ? $rowldap[4] : "",
    filter => "(".$rowldap[6]."=%s)"
  );

  return 1 if $ldap->authenticate($username, $password);
  return 0;
}

sub get_permissions {
  my $dbh = shift;
  my $redmine_user = shift;
  my $project_id = shift;

  my $sth = $dbh->prepare(
    "SELECT r.permissions FROM projects p, users u, members m, roles r, member_roles mr
     WHERE p.identifier=? AND u.login=? AND m.project_id=p.id AND m.user_id=u.id AND mr.member_id = m.id AND r.id=mr.role_id
     UNION
     SELECT r.permissions FROM projects p, roles r
     WHERE p.identifier=? AND p.is_public=1 AND r.id=1;"
  );

  $sth->execute($project_id, $redmine_user, $project_id);
  my $permissions = $sth->fetchrow_array;
  $sth->finish();

  return $permissions;
}

sub can_read {
  my $permissions = shift;
  return 0 unless $permissions;
  return $permissions =~ /:browse_repository/ ? 1 : 0;
}

sub can_write {
  my $permissions = shift;
  return 0 unless $permissions;
  return $permissions =~ /:commit_access/ ? 1 : 0;
}

sub cache {
  my $redmine_user = shift;
  my $redmine_pass = shift;
  my $project_id = shift;
  my $is_read_only = shift;

  my $key = $redmine_user . $redmine_pass . $project_id . $is_read_only;

  $AUTH_CACHE{$key} = time();
}

sub is_cached {
  my $redmine_user = shift;
  my $redmine_pass = shift;
  my $project_id = shift;
  my $is_read_only = shift;

  my $key = $redmine_user . $redmine_pass . $project_id . $is_read_only;

  return 1 if exists($AUTH_CACHE{$key}) && $AUTH_CACHE{$key} >= time() - 60;
  return 0;
}

sub connect_database {
  return DBI->connect($DSN, $DB_USER, $DB_PASS);
}

1;
