Chore Account server

Check-in [85e1fec511]
Login

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

Overview
Comment:Initial dev snapshot
Downloads: Tarball | ZIP archive | SQL archive
Timelines: family | descendants | master | trunk
Files: files | file ages | folders
SHA3-256:85e1fec511107df123933bb493cd1583ff63476b093cc505bc7097a938540370
User & Date: ajv-899-334-8894@vsta.org 2016-10-24 18:07:30
Context
2016-10-26
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

Added .gitignore.







>
>
>
1
2
3
chore
*.pyc
*.real

Added etc/accounts.





































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# Accounts and services
# Sample file

account *family
    serve "Web Radio"
    serve "Web Player"
    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"

Added etc/config.





































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#
# config
#       Static configuration

# Domain, if multiple on one server
# domain "chore"

# Web interface
serve http
    port 8000

# Accounts file, which will be reloaded when its modification
#  is detected
accounts "etc/accounts"

# Path to our Unix-domain datagram service socket
# Default is /tmp/accounts-<domain>
# listen "/tmp/some-other-name"

Added get.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
#
# 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.
    #
    # This lets us check here to see if the cookie is OK; if so,
    #  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()

Added main.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
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
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
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
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
#
# 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('-')
    if len(tup) != 2:
        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.
#
# Returns (Config, cfg{}, mtime)
#  "mtime" is the os.stat time for file modification, so we can detect
#  changes to the config file and reload.
def load_accounts(fn):
    c = chore.config.Config()
    for e in (
         ("account", "serve"), ("account", "serve", "path"),
         ("account", "name"), ("account", "email"),
         ("account", "pass"), ("domain,") ):
        c.onearg.add(e)

    # Multiple accounts (of course)
    # Each is "account X [templates...]"
    c.args.add( ("account",) )
    c.mults.add( ("account",) )

    # Each account can have multiple services
    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)

    # Protection domain, and UDS for internal communication
    for e in ( ("domain",), ("accounts",), ("listen",) ):
        c.onearg.add(e)

    cfg = c.load_cfg(fn)
    return cfg

# Account web interface
#
# 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.
#  There is also a var/cookies/ subdir which holds the cookie active
#  for each user.
# This server acts as a portal, tabulating configured services for
#  any particular user.  By coming through this portal, users can
#  also refresh expiring cookies via a new round of authentication
#  before landing on their desired service.
#
# cfg{} - Top-level configuration
# accts{},accts_tm - Config of accounts, and st_mtime when read
# nonces{} - Map from server ID to nonce value (to detect
#       corrupt or mis-directed traffic)
class AccountsServer(chore.server.Server):
    def __init__(self, cfg, domain="chore"):
        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)

    # If account file is newer, reload
    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
            if resp in self.nonces:
                if self.nonces[resp] != req["nonce"]:
                    sys.stderr.write("Bad nonce from '%s'\n" %
                        (resp,))
                    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")

    # Dedicated service loop for UDS operations
    def serve_uds(self):

        # Service socket
        cfg = self.cfg
        dom = cfg.get("domain", "chore")
        nm = "/tmp/acct-%s" % (dom,)
        try:
            # Old instance
            os.unlink(nm)
        except:
            pass
        s = socket.socket(socket.AF_UNIX, socket.SOCK_DGRAM)
        s.bind(nm)
        sys.stderr.write("Account server for %s running\n" % (dom,))

        # Service loop
        while True:
            # Next request
            buf = s.recv(65536)

            # Decode request
            try:
                req = json.loads(buf)
            except:
                sys.stderr.write("acct-%s: Invalid JSON %s\n" %
                    (dom, buf))
                continue

            # Reload accounts, maybe
            self.check_accounts()

            try:
                self.serve(req)
            except:
                sys.stderr.write("acct-%s: failed request %s\n" %
                    (dom, req))
                continue

    # Activate this account server
    def run(self):

        # Our web interface(s)
        self.start_http()

        # A thread for UDS services
        t = threading.Thread(target=self.serve_uds)
        t.start()

if __name__ == "__main__":
    if len(sys.argv) != 2:
	sys.stderr.write("Usage is: %s <config-file>\n" %
	    (sys.argv[0],))
	sys.exit(1)

    # Create the server with config
    cfg = load_cfg(sys.argv[1])
    app = AccountsServer(cfg)

    # Fire it up
    app.run()

Added var/.gitignore.



>
1
cookies