Chore Account server

Check-in [8fa03b3227]
Login

Many hyperlinks are disabled.
Use anonymous login to enable hyperlinks.

Overview
Comment:Bring up basic authen
Downloads: Tarball | ZIP archive | SQL archive
Timelines: family | ancestors | descendants | both | master | trunk
Files: files | file ages | folders
SHA3-256:8fa03b32278772bb85e658935e383bf71fa899ae7e5592076e354ef9de1b896b
User & Date: ajv-899-334-8894@vsta.org 2016-10-26 22:11:24
Context
2016-10-26
23:13
Drop some pdb hooks. Add version to redir target to sidestep cache check-in: 80164f8171 user: ajv-899-334-8894@vsta.org tags: master, trunk
22:11
Bring up basic authen check-in: 8fa03b3227 user: ajv-899-334-8894@vsta.org tags: master, trunk
2016-10-24
18:07
Initial dev snapshot check-in: 85e1fec511 user: ajv-899-334-8894@vsta.org tags: master, trunk
Changes
Hide Diffs Unified Diffs Ignore Whitespace Patch

Changes to etc/accounts.

7
8
9
10
11
12
13

14
15
16
17
18
    serve "ePub Reader"
        path "/var/www/books"

account dad *family
    name "The Paternal Unit"
    email "someone@domain.com"
    pass "daddy123"


account mom *family
    name "The Maternal Unit"
    email "cosmic@big-universe.com"
    pass "momEE123"







>





7
8
9
10
11
12
13
14
15
16
17
18
19
    serve "ePub Reader"
        path "/var/www/books"

account dad *family
    name "The Paternal Unit"
    email "someone@domain.com"
    pass "daddy123"
    serve "Private Service"

account mom *family
    name "The Maternal Unit"
    email "cosmic@big-universe.com"
    pass "momEE123"

Changes to get.py.

1
2
3
4

5
6
7
8
9
10
11
..
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

























#
# get.py
#       Account portal & service interface
#


class GET_mixin(object):
    # Top level
    #
    # We don't use chore-level authentication, as we're the
    #  ones who will deal with authentication in the first place.
    #
................................................................................
    #  we either display the portal (if they pointed their own
    #  browser at us) or else generate a redir to the service which
    #  kicked them over here to get (re-)authentcated.
    #
    # If there's no cookie, or not a good one, let them enter
    #  it here and we'll deal with authentication.
    def send_top(self):

        res = self.auth_cookie()

        # If they're OK...
        if res is True:
            # Show the service portal
            return self.send_portal()
            # TBD, redirect back.  But have to be really careful
            #  about validating destination; we don't want to send
            #  them anywhere but our own services.



















    # Show portal of services
    def send_portal(self):
        app = self.server.approot


        buf = self.build_header("Service Portal")
        buf += "<h3>Choose a service:</h3><br>\n"
        for account in app.config.get("account", ()):
            import pdb



            pdb.set_trace()





























>







 







>




<
<



>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>



|
>



<
|
>
>
>
|
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
..
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
#
# get.py
#       Account portal & service interface
#
import pdb

class GET_mixin(object):
    # Top level
    #
    # We don't use chore-level authentication, as we're the
    #  ones who will deal with authentication in the first place.
    #
................................................................................
    #  we either display the portal (if they pointed their own
    #  browser at us) or else generate a redir to the service which
    #  kicked them over here to get (re-)authentcated.
    #
    # If there's no cookie, or not a good one, let them enter
    #  it here and we'll deal with authentication.
    def send_top(self):
        pdb.set_trace()
        res = self.auth_cookie()

        # If they're OK...
        if res is True:


            # TBD, redirect back.  But have to be really careful
            #  about validating destination; we don't want to send
            #  them anywhere but our own services.

            # Show the service portal
            return self.send_portal()

        # Please log in...
        buf = self.build_header("Please Log In")
        if self.vals:
            pdb.set_trace()
            buf += "<pre>%s</pre><br>" % (self.vals["msg"],)
        buf += "<h3>Please log in:</h3><br>\n"
        buf += '<form action="/login" method="post">\n'
        buf += ' Account name: <input type="text" name="acct"><br>\n'
        buf += ' Password: <input type="password" name="pw"><br>\n'
        buf += ' <input type="submit" value="Log In">\n'
        buf += "</form>\n"
        buf = self.build_tailer(buf)

        return buf

    # Show portal of services
    def send_portal(self):
        server = self.server
        app = server.approot

        buf = self.build_header("Service Portal")
        buf += "<h3>Choose a service:</h3><br>\n"

        cfg = app.accts.get(self.user)
        if cfg is None:
            raise Exception, "Unknown account authenticated"
        services = cfg.get("serve")
        pdb.set_trace()

        # Hostname they used to reach us, should be
        #  <host>:<port#>
        host = self.headers.get("host").split(":")[0]
        if services is None:
            buf += "<i>No services are configured for you?</i>"
        else:
            buf += "<ul>\n"
            for nm,cfg in services.iteritems():
                # Get service configuration; we need their port number
                #  to build the URL our client can use to reach it.
                if nm not in app.services:
                    # Don't list services which are not started
                    continue
                sport = app.services[nm][1]

                # Service URL
                surl = "http%s://%s:%d" % \
                 (("s" if server.ssl else ""), host, sport)
                buf += ' <li><a href="%s">%s</a></li>\n' % (surl, nm)

            buf += "</ul>\n"
        buf = self.build_tailer(buf)

        return buf

Changes to main.py.

1
2
3
4
5
6
7

8
9
10
11
12
13
14
..
16
17
18
19
20
21
22

























23
24
25
26
27
28
29
..
48
49
50
51
52
53
54
55






















56
57
58
59
60
61
62
63
64
..
74
75
76
77
78
79
80
81
82
83
84
85

86
87
88
89
90
91
92
93
...
107
108
109
110
111
112
113




114
115
116
117
118
119
120
...
122
123
124
125
126
127
128
129
130

131
132
133
134
135
136
137
138
139
140
141
142


143
144
145
146
147
148
149
...
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
#
# account.py
#	Manage user accounts across chore services
#
import sys, os, threading, socket
import chore
from get import GET_mixin


# Trap monkey business with UDS path names
def okname(s):
    if not s.startswith("/tmp/"):
        return False
    s = s[5:]
    tup = s.split('-')
................................................................................
        return False
    for s in tup:
        if not s:
            return False
        if any( (not c.isalnum()) for c in s ):
            return False
    return True


























# Load account data
#
# This deals with user/pass, but also which services a given user
#  may access.  Accounts of the form "*name" are not usable accounts,
#  but may be used as a template to apply the same account options
#  to multiple actual accounts.
................................................................................
    c.mults.add( ("account", "serve") )

    # Load the file
    f = open(fn, "r")
    ftm = os.fstat(f.fileno()).st_mtime
    cfg = c.load_cfg_file(f)
    f.close()























    # Result, and file time at which we read this config
    return cfg,ftm

# Load main config
def load_cfg(fn):
    c = chore.config.Config()

    # Web interfae
    chore.www.add_config(c)
................................................................................
#
# We act as a portal to the chore-based services.  As a common
#  starting point, we are also able to deal with authentication
#  and cookie management.  Our goal is to have a single authentication
#  cookie across all the service, with per-user control of which
#  services will be available.
class Account_Handler(chore.handlers.Chore_Handler, GET_mixin,
        chore.authen.Authen_mixin):

    def __init__(self, conn, tup, approot):
        chore.handlers.Chore_Handler.__init__(self, conn, tup,
            approot, (GET_mixin.__init__,

            chore.authen.Authen_mixin.__init__))

# Accounts server
# One of these will typically be spun up per installation; it provides
#  the connection from (per-host) cookies to the user profile for that
#  cookie for a specific service on the host.
# The accounts are in a typical chore config file, which is expected
#  to be inside this directory, and protected from access by others.
................................................................................
        self.domain = domain

        # Static account config
        self.cfg = cfg

        # Per-server nonce values
        self.nonces = {}





        # Load current version of accounts
        self.accts,self.accts_tm = load_accounts(cfg["accounts"])

        # Let Chore code set up the rest
        chore.server.Server.__init__(self, cfg, Account_Handler)

................................................................................
    def check_accounts(self):
        st = os.stat(self.cfg["accounts"])
        if st.st_mtime > self.accts_tm:
            # Yup, reload
            self.accts,self.accts_tm = load_accounts(cfg["accounts"])

    # Send back a reply to a Unix-domain socket
    # Return status "OK" if no error specified
    def reply(self, dest, op, err=None):

        if not okname(dest):
            raise Exception, "Illegal reply name: %s" % (dest,)
        s = socket.socket(socket.AF_UNIX, socket.SOCK_DGRAM)
        s.connect(dest)
        res = {"op": op, "result": (err or "OK")}
        s.send(json.dumps(res))
        s.close()

    # Process this request.  The request is a dict extracted
    #  from JSON, with at least:
    # sender - socket for answer
    # op - string name of operation


    def serve(self, req):
        op = req["op"]
        resp = req["reply-to"]

        # Start of service
        if op == "start":
            # Check nonce if needed
................................................................................
                    self.reply(resp, req["op"], "?Nonce")
                    return
            self.nonces[resp] = req["nonce"]

            # Register service
            self.services[req["service"]] = \
             (req["pid"], req["port"], resp)
            self.reply(resp, req["op"])
            return

        # Unknown
        sys.stderr.write("Unknown op '%s' from '%s'\n" %
            (op, resp))
        self.reply(resp, req["op"], "?Bad-op")








>







 







>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>







 








>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>

|







 







|




>
|







 







>
>
>
>







 







|
|
>




|
|




|

>
>







 







|







1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
..
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
..
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
...
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
...
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
...
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
...
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
#
# account.py
#	Manage user accounts across chore services
#
import sys, os, threading, socket
import chore
from get import GET_mixin
from post import POST_mixin

# Trap monkey business with UDS path names
def okname(s):
    if not s.startswith("/tmp/"):
        return False
    s = s[5:]
    tup = s.split('-')
................................................................................
        return False
    for s in tup:
        if not s:
            return False
        if any( (not c.isalnum()) for c in s ):
            return False
    return True

# Overlay an account config
def merge_account(src, dest):
    for k,v in src.iteritems():

        # Get a tree copy for the value if it isn't
        #  there yet
        if k not in dest:
            dest[k] = v
            continue

        # Merge sub-dicts
        v2 = dest[k]
        if isinstance(v2, dict):
            assert isinstance(v, dict)
            merge_account(v, v2)
            continue

        # Some sort of collation?
        if isinstance(v2, (tuple,list)):
            v2 = list(v2)
            raise Exception, "Unhandled config merge"

        # Otherwise the first value, wins
        continue

# Load account data
#
# This deals with user/pass, but also which services a given user
#  may access.  Accounts of the form "*name" are not usable accounts,
#  but may be used as a template to apply the same account options
#  to multiple actual accounts.
................................................................................
    c.mults.add( ("account", "serve") )

    # Load the file
    f = open(fn, "r")
    ftm = os.fstat(f.fileno()).st_mtime
    cfg = c.load_cfg_file(f)
    f.close()

    # Transform config dict into something more immediately
    #  useful; keyed by account name, with prototype references
    #  folded into the account
    accts = {}
    for tupname,cfg in cfg["account"]:
        # Remap config

        # Service list, turn into dict keyed by service name,
        #  config as value.
        serve = {}
        for snm,scfg in cfg.get("serve", ()):
            serve[snm] = scfg
        cfg["serve"] = serve

        # Start with specific config
        accts[tupname[0]] = cfg

        # Fold on any templates
        for nm in tupname[1:]:
            templ = accts[nm]
            merge_account(templ, cfg)

    # Result, and file time at which we read this config
    return accts,ftm

# Load main config
def load_cfg(fn):
    c = chore.config.Config()

    # Web interfae
    chore.www.add_config(c)
................................................................................
#
# We act as a portal to the chore-based services.  As a common
#  starting point, we are also able to deal with authentication
#  and cookie management.  Our goal is to have a single authentication
#  cookie across all the service, with per-user control of which
#  services will be available.
class Account_Handler(chore.handlers.Chore_Handler, GET_mixin,
        POST_mixin, chore.authen.Authen_mixin):

    def __init__(self, conn, tup, approot):
        chore.handlers.Chore_Handler.__init__(self, conn, tup,
            approot, (GET_mixin.__init__,
                POST_mixin.__init__,
                chore.authen.Authen_mixin.__init__))

# Accounts server
# One of these will typically be spun up per installation; it provides
#  the connection from (per-host) cookies to the user profile for that
#  cookie for a specific service on the host.
# The accounts are in a typical chore config file, which is expected
#  to be inside this directory, and protected from access by others.
................................................................................
        self.domain = domain

        # Static account config
        self.cfg = cfg

        # Per-server nonce values
        self.nonces = {}

        # Services, as they register to us.
        # Map from service name to (PID, port#, socket-name)
        self.services = {}

        # Load current version of accounts
        self.accts,self.accts_tm = load_accounts(cfg["accounts"])

        # Let Chore code set up the rest
        chore.server.Server.__init__(self, cfg, Account_Handler)

................................................................................
    def check_accounts(self):
        st = os.stat(self.cfg["accounts"])
        if st.st_mtime > self.accts_tm:
            # Yup, reload
            self.accts,self.accts_tm = load_accounts(cfg["accounts"])

    # Send back a reply to a Unix-domain socket
    # Our response is the request with a "result" field added
    def reply(self, result, req):
        dest = req["reply-to"]
        if not okname(dest):
            raise Exception, "Illegal reply name: %s" % (dest,)
        s = socket.socket(socket.AF_UNIX, socket.SOCK_DGRAM)
        s.connect(dest)
        req["result"] = result
        s.send(json.dumps(req))
        s.close()

    # Process this request.  The request is a dict extracted
    #  from JSON, with at least:
    # reply-to - socket for answer
    # op - string name of operation
    # nonce - Random value, invariant from this sender
    #  over the life of its service.
    def serve(self, req):
        op = req["op"]
        resp = req["reply-to"]

        # Start of service
        if op == "start":
            # Check nonce if needed
................................................................................
                    self.reply(resp, req["op"], "?Nonce")
                    return
            self.nonces[resp] = req["nonce"]

            # Register service
            self.services[req["service"]] = \
             (req["pid"], req["port"], resp)
            self.reply("OK", req)
            return

        # Unknown
        sys.stderr.write("Unknown op '%s' from '%s'\n" %
            (op, resp))
        self.reply(resp, req["op"], "?Bad-op")

Added post.py.







































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
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
#
# post.py
#	HTML POST/PUT handling
#
# /login
#       Form post, user/pass entered
#
import urllib, sys

class POST_mixin(object):

    def __init__(self):
        self.dispatchers.append( ("POST", self.post_login) )

    # Back to user/pass screen, with error message
    def failed_login(self, msg):
        msg = urllib.quote(msg)
        return True,self.send_redir("/?msg=%s" % (msg,))

    # They've entered a user,pass, see if it's OK
    def post_login(self, buf):
        app = self.server.approot

        # Decode form fields
        self.parseKV(buf)

        # Valid form input?
        vals = self.vals
        user = vals.get("acct")
        pw = vals.get("pw")
        if (not user) or (not pw):
            return False,None
        sys.stderr.write("login %s\n" % (user,))

        # Look up user
        import pdb
        pdb.set_trace()
        acct = app.accts.get(user)
        if acct is None:
            return self.failed_login("Invalid user or password")

        # TBD, hashed passwords
        acctpw = acct.get("pass")
        if acctpw is None:
            return self.failed_login("Account is locked")
        if pw != acctpw:
            return self.failed_login("Invalid user or password")

        # Ok, they're on
        self.new_cookie(user)
        return True,self.send_redir("/")