aboutsummaryrefslogtreecommitdiffstats
path: root/lib/Fripost/Session.pm
blob: a451c4ad2a7efa62f43cf4c53319da5314f14df5 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
#----------------------------------------------------------------------
# Fripost admin panel - ephemeral sessions
# Copyright © 2018 Fripost
# Copyright © 2018 Guilhem Moulin <guilhem@fripost.org>
#
# This program is free software: you can redistribute it and/or modify it
# under the terms of the GNU Affero 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 Affero General Public
# License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
#----------------------------------------------------------------------

package Fripost::Session v0.0.1;
use warnings;
use strict;

use Authen::SASL ();
use Net::LDAP::Constant qw/LDAP_SUCCESS LDAP_ALREADY_EXISTS/;
use Net::LDAP::Extension::Refresh ();
use Net::LDAP::Util "escape_dn_value";

use Crypt::URandom "urandom";

use Fripost ();


# new(Fripost object)
#   Create a new ephemeral session from a Fripost object, and return
#   suitable credentials for later SASL proxy authorization.
sub new($$) {
    my ($class, $fp) = @_;

    # don't base64-encode but hex-encode as the commonName is case-insensitive
    my $id  = unpack("H*", urandom(16));
    my $dn = sprintf($fp->{_config}->{ldap}->{"session-authcDN"},
                escape_dn_value($id));
    # hex-encode the password too since we can't have NUL bytes in the SASL packet
    my $password = unpack("H*", urandom(16));

    my $authzid = $fp->whoami() // die;
    die "Invalid identity: $authzid\n" unless $authzid =~ /\Adn:/;

    my @attrs = (objectClass => [ qw/organizationalRole simpleSecurityObject dynamicObject/ ]);
    # libsasl2 requires {CLEARTEXT} passwords, even for PLAIN, cf.
    # https://openldap.org/lists/openldap-technical/201310/msg00007.html
    # (not a big deal here though since our shared secrets are internal
    # and ephemeral)
    push @attrs, userPassword => ( "{CLEARTEXT}" . $password );

    my $r = $fp->{_ldap}->add($dn, attrs => \@attrs);
    if ($r->code == LDAP_ALREADY_EXISTS) {
        # try to delete the entry (we're not allowed to modify existing entries)
        my $r2 = $fp->{_ldap}->delete($dn);
        $r = $fp->{_ldap}->add($dn, attrs => \@attrs)
            if $r2->code == LDAP_SUCCESS;
    }
    $fp->croak("LDAP error code %i: %s\n", $r->code, $r->error)
        unless $r->code == LDAP_SUCCESS;

    my %creds = (authcid => $id, password => $password, authzid => $authzid);
    bless \%creds, $class;
}

# authenticate(OPTION => VALUE, ..)
#   Create a new Fripost object and return it after authentication
#   (using SASL proxy authorization with the ephemeral credentials).
#   If the "refresh" is set (the default), then TTL value of the entry
#   on the backup is refreshed.
sub authenticate($%) {
    my $self = shift;
    my %conf = @_;

    my $refresh = delete $conf{refresh} // 1;
    my $authcid = sprintf($conf{ldap}->{"session-authcID"} // "%s",
            $self->{authcid});

    my $sasl = Authen::SASL::->new( mechanism => "PLAIN", callback  => {
         user => $authcid
       , pass => $self->{password}
       , authname => $self->{authzid}
    }) or die "Creation of Authen::SASL object failed";

    my $fp = Fripost::->new(%conf);
    my $r = $fp->{_ldap}->bind(undef, sasl => $sasl);
    $fp->croak("LDAP error code %i: %s\n", $r->code, $r->error)
        unless $r->code == LDAP_SUCCESS;
    
    if ($refresh) {
        my $dn = sprintf($conf{ldap}->{"session-authcDN"} // "%s",
            escape_dn_value($self->{authcid}));
        my $ttl = $conf{www}->{"cache-expires"};
        $r = $fp->{_ldap}->refresh(entryName => $dn, requestTtl => $ttl);
        $fp->croak("LDAP error code %i: %s\n", $r->code, $r->error)
            unless $r->code == LDAP_SUCCESS;
    }
    return $fp;
}

# destroy(OPTION => VALUE, ..)
#   Create a new Fripost object, authenticate (using SASL proxy
#   authorization), and delete the entry on the LDAP backend.
#   The object shouldn't be used after using this method.
sub destroy($%) {
    my $self = shift;
    my %conf = @_;

    my $dn = sprintf($conf{ldap}->{"session-authcDN"} // "%s",
        escape_dn_value($self->{authcid}));

    my $fp = authenticate($self, %conf, refresh => 0);
    my $r = $fp->{_ldap}->delete($dn);
    $fp->croak("LDAP error code %i: %s\n", $r->code, $r->error)
        unless $r->code == LDAP_SUCCESS;

   # forget credentials in the object (now a blessed empty hash reference)
   undef %$self;
}

1;