By using this site, you agree to our updated Privacy Policy and our Terms of Use. Manage your Cookies Settings.
434,637 Members | 1,945 Online
Bytes IT Community
+ Ask a Question
Need help? Post your question and get tips & solutions from a community of 434,637 IT Pros & Developers. It's quick & easy.

How to protect Python source from modification

P: n/a
Hi all

I am writing a multi-user accounting/business system. Data is stored in
a database (PostgreSQL on Linux, SQL Server on Windows). I have written
a Python program to run on the client, which uses wxPython as a gui,
and connects to the database via TCP/IP.

The client program contains all the authentication and business logic.
It has dawned on me that anyone can bypass this by modifying the
program. As it is written in Python, with source available, this would
be quite easy. My target market extends well up into the mid-range, but
I do not think that any CFO would contemplate using a program that is
so open to manipulation.

The only truly secure solution I can think of would involve a radical
reorganisation of my program, so I am writing to see if anyone has a
simpler suggestion. Here is the idea.

1. Write a socket server program that runs on the server. The socket
server is the only program that connects to the database. The client
program connects to the server, which authenticates the client against
the database and then listens for requests from the client.

2. Devise my own protocol for communication between client and server.
For selects, the client sends a request, the server checks permissions,
then retrieves the data from the database and passes it to the client.
For updates, the client passes up the data to be updated, the server
checks it against its business logic, and then updates the database.

There is the question of where state should be maintained. If on the
server, I would have to keep all the client/server connections open,
and maintain the state of all the sessions, which would put quite a
load on the server. If on the client, I would have to reorganise my
thinking even more, but this would have an advantage - I will
eventually want to write a browser interface, and this would be much
closer in concept, so the two approaches would be quite similar.

This raises the question of whether I should even bother with a gui
client, or bite the bullet and only have a browser based front end.
Judging from recent comments about new technologies such as Ajax, a lot
of the disadvantages have been overcome, so maybe this is the way to
go.

It would be a shame to scrap all the effort I have put into my
wxPython-based front end. On the other hand, it would be pointless to
continue with an approach that is never going to give me what I want.
Any advice which helps to clarify my thinking will be much appreciated.

Thanks

Frank Millman

Sep 12 '05 #1
Share this Question
Share on Google+
29 Replies


P: n/a
Frank Millman wrote:
Hi all

I am writing a multi-user accounting/business system. Data is stored in
a database (PostgreSQL on Linux, SQL Server on Windows). I have written
a Python program to run on the client, which uses wxPython as a gui,
and connects to the database via TCP/IP.

The client program contains all the authentication and business logic.
It has dawned on me that anyone can bypass this by modifying the
program. As it is written in Python, with source available, this would
be quite easy. My target market extends well up into the mid-range, but
I do not think that any CFO would contemplate using a program that is
so open to manipulation. [...]


My suggestion is to use py2exe or cx_Freeze to package your application.
It's then not as trivial to modify it. Btw. you don't need to ship the
..py source code files, it's enough to ship only .pyc bytecode files.

Using py2exe it's not even obvious that your application is written in
Python at all.

It's not a silver bullet, but at least it makes recompiling/modifiying
your app not easier than with Java (and/or .NET I suppose).

That being said, even if you continue with the GUI approach, it may
still be a good idea to factor out all the business logic in a separate
module so you can eventually switch to a web application or a three-tier
model without too much effort.

Also, there's no need at all to put in countless hours implementing your
own network protocol. If you really want to separate client and app
server, then why not use something simple as PyRO, or even XML/RPC.

HTH,

-- Gerhard

Sep 12 '05 #2

P: n/a
Frank Millman wrote:
I am writing a multi-user accounting/business system. Data is stored in
a database (PostgreSQL on Linux, SQL Server on Windows). I have written
a Python program to run on the client, which uses wxPython as a gui,
and connects to the database via TCP/IP.

The client program contains all the authentication and business logic.
It has dawned on me that anyone can bypass this by modifying the
program. As it is written in Python, with source available, this would
be quite easy. My target market extends well up into the mid-range, but
I do not think that any CFO would contemplate using a program that is
so open to manipulation.

The only truly secure solution I can think of would involve a radical
reorganisation of my program


Please define what "truly secure" means to you.

I think you'll find that the only "truly secure" solution is to install
the critical authentication and business logic stuff that you want to
protect on a server to which the user does not have physical access.

People wanting to protect critical algorithms often conclude that they
need to have a "black box" server which cannot be physically opened by
the user.

Or do you think this issue is in some way unique to Python? You might
not realize that the only difference from a security point of view
between shipping such a program written in Python and one written in,
say, C++, is that "modifying the program" is somewhat more tedious with
C++. That's no better than security by obscurity; maybe it should be
called "security by adiposity". ;-)

But the real answer does depend a lot on *exactly* what kind of security
you want (or, ultimately, what it turns out you really need, once you've
clarified your thinking based on the feedback you do get here). Issues
like: are you more concerned about detecting changes, or in preventing
them in the first place? (the latter is much harder); what is the nature
of software that competes with yours? (is it really any more secure, or
only apparently so? maybe this is just a marketing issue); and is there
any intellectual property that you are trying to protect here, or are
you just interested in avoiding casual disruption of normal operation?

-Peter
Sep 12 '05 #3

P: n/a

Gerhard Häring wrote:
Frank Millman wrote:
Hi all

I am writing a multi-user accounting/business system. Data is stored in
a database (PostgreSQL on Linux, SQL Server on Windows). I have written
a Python program to run on the client, which uses wxPython as a gui,
and connects to the database via TCP/IP.

The client program contains all the authentication and business logic.
It has dawned on me that anyone can bypass this by modifying the
program. As it is written in Python, with source available, this would
be quite easy. My target market extends well up into the mid-range, but
I do not think that any CFO would contemplate using a program that is
so open to manipulation. [...]
My suggestion is to use py2exe or cx_Freeze to package your application.
It's then not as trivial to modify it. Btw. you don't need to ship the
.py source code files, it's enough to ship only .pyc bytecode files.

Using py2exe it's not even obvious that your application is written in
Python at all.

It's not a silver bullet, but at least it makes recompiling/modifiying
your app not easier than with Java (and/or .NET I suppose).


My problem is that, if someone has access to the network and to a
Python interpreter, they can get hold of a copy of my program and use
it to knock up their own client program that makes a connection to the
database. They can then execute any arbitrary SQL command.
That being said, even if you continue with the GUI approach, it may
still be a good idea to factor out all the business logic in a separate
module so you can eventually switch to a web application or a three-tier
model without too much effort.

Agreed
Also, there's no need at all to put in countless hours implementing your
own network protocol. If you really want to separate client and app
server, then why not use something simple as PyRO, or even XML/RPC.

Perhaps 'protocol' is the wrong word. I already have a simple socket
server program running. If explain how I do it, perhaps you can
indicate whether PyRO or XML/RPC would make my life easier.

The server program is currently programmed to accept a number of
message types from the client program. Each message's data string
starts with a numeric prefix, which indicates the type of message,
followed by a pickled tuple of arguments. The server program reads the
string, extracts the numeric prefix, and passes the rest of the string
to the appropriate function using a subthread.

For example, I keep track of who is currently logged in. On startup,
the client connects to my server and sends a '1' followed by their
userid and other information. The server receives this and passed the
data to a 'login' function, which uses a Python dictionary to store the
information. If the server detects that the user is already logged in,
it sends back an error code and the client program displays a message
and terminates. Otherwise it sends back an 'ok' code, and the client
can continue. When the client logs off, it sends a '2' followed by
their userid, which the server receives and passes it to a 'logoff'
function, which deletes the entry from the dictionary.

The system of numeric prefixes and associated data string making up a
message is what I mean by a protocol.
HTH,

-- Gerhard


Thanks

Frank

Sep 12 '05 #4

P: n/a
Frank Millman wrote:
Hi all

I am writing a multi-user accounting/business system. Data is stored in
a database (PostgreSQL on Linux, SQL Server on Windows). I have written
a Python program to run on the client, which uses wxPython as a gui,
and connects to the database via TCP/IP.

The client program contains all the authentication and business logic.
It has dawned on me that anyone can bypass this by modifying the
program.
If your program relies on a RDBMS, then it's the RDBMS job to enforce
security rules.
As it is written in Python, with source available, this would
be quite easy.


Then there's probably something wrong with the way you manage security.

NB: splitting business logic from the GUI is still a good idea anyway.

--
bruno desthuilliers - unpythonic sig:
python -c "print '@'.join(['.'.join([w[::-1] for w in p.split('.')]) for
p in 'o****@xiludom.gro'.split('@')])"
Sep 12 '05 #5

P: n/a

Peter Hansen wrote:
Frank Millman wrote:
I am writing a multi-user accounting/business system. Data is stored in
a database (PostgreSQL on Linux, SQL Server on Windows). I have written
a Python program to run on the client, which uses wxPython as a gui,
and connects to the database via TCP/IP.

The client program contains all the authentication and business logic.
It has dawned on me that anyone can bypass this by modifying the
program. As it is written in Python, with source available, this would
be quite easy. My target market extends well up into the mid-range, but
I do not think that any CFO would contemplate using a program that is
so open to manipulation.

The only truly secure solution I can think of would involve a radical
reorganisation of my program
Please define what "truly secure" means to you.


Fair question. I am not expecting 'truly' to mean 100% - I know that is
impossible. I will try to explain.

Here are some assumptions -
1. A system adminstrator is responsible for the system.
2. There is a single userid and password for connecting to the
database. This must be stored somewhere so that the client program can
read it to generate the appropriate connection string. The users do not
need to know this userid and password.
3. Each user has their own userid and password, which is stored in the
database in a 'users' table. I use this in my program for
authentication when a user tries to connect.
4. The client program can be run from anywhere on the network that has
access to the program and to a Python interpreter.

[snip]

But the real answer does depend a lot on *exactly* what kind of security
you want (or, ultimately, what it turns out you really need, once you've
clarified your thinking based on the feedback you do get here). Issues
like: are you more concerned about detecting changes, or in preventing
them in the first place? (the latter is much harder); what is the nature
of software that competes with yours? (is it really any more secure, or
only apparently so? maybe this is just a marketing issue); and is there
any intellectual property that you are trying to protect here, or are
you just interested in avoiding casual disruption of normal operation?

I am not concerned about anyone reading my code - in fact I am looking
forward to releasing the source and getting some feedback.

My concern is this. I have all this fancy authentication and business
logic in my program. If someone wants to bypass this and get direct
access to the database, it seems trivially easy. All they have to do is
read my source, find out where I get the connection string from, write
their own program to make a connection to the database, and execute any
SQL command they want.

If I move all the authentication and business logic to a program which
runs on the server, it is up to the system administrator to ensure that
only authorised people have read/write/execute privileges on that
program. Clients will have no privileges, not even execute. They will
have their own client program, which has to connect to my server
program, and communicate with it in predefined ways. I *think* that in
this way I can ensure that they cannot do anything outside the bounds
of what I allow them.

The only problem is that this is very different from the way my program
works at present, so it will be quite a bit of work to re-engineer it.
If someone can suggest a simpler solution obviously I would prefer it.
But if the consensus is that I am thinking along the right lines, I
will roll up my sleeves and get stuck in.
-Peter


I hope this explains my thinking a bit better.

Thanks

Frank

Sep 12 '05 #6

P: n/a
Frank Millman wrote:
Peter Hansen wrote:
Frank Millman wrote:
(snip)
The only truly secure solution I can think of would involve a radical
reorganisation of my program
Please define what "truly secure" means to you.

Fair question. I am not expecting 'truly' to mean 100% - I know that is
impossible. I will try to explain.

Here are some assumptions -
1. A system adminstrator is responsible for the system.
2. There is a single userid and password for connecting to the
database. This must be stored somewhere so that the client program can
read it to generate the appropriate connection string. The users do not
need to know this userid and password.
3. Each user has their own userid and password,
which is stored in the
database in a 'users' table. I use this in my program for
authentication when a user tries to connect.


Why not simply using the security system of your RDBMS ? If you set up
appropriate privileges in the RDBMS, you won't have to store any
userid/password in the program, and no user will be able to bypass
anything, even if connecting directly (like with a CLI DB client) to the
RDBMS.

[snip]

(snip more)
I am not concerned about anyone reading my code - in fact I am looking
forward to releasing the source and getting some feedback.

My concern is this. I have all this fancy authentication and business
logic in my program. If someone wants to bypass this and get direct
access to the database, it seems trivially easy. All they have to do is
read my source, find out where I get the connection string from, write
their own program to make a connection to the database, and execute any
SQL command they want.


That's why RDBMS have an authentication and security system. This
doesn't means your program doesn't have or cannot add it's own security
management, but it should be based on the RDBMS one.

--
bruno desthuilliers
python -c "print '@'.join(['.'.join([w[::-1] for w in p.split('.')]) for
p in 'o****@xiludom.gro'.split('@')])"
Sep 12 '05 #7

P: n/a
On 12 Sep 2005 08:33:10 -0700, "Frank Millman" <fr***@chagford.com>
declaimed the following in comp.lang.python:

My problem is that, if someone has access to the network and to a
Python interpreter, they can get hold of a copy of my program and use
it to knock up their own client program that makes a connection to the
database. They can then execute any arbitrary SQL command.
If your DBMS is directly accessible on the net, you're vulnerable
even without Python. Especially if you have "authentication" logic being
done at the client end. There is nothing to prevent someone using a
compatible query browser or command-line utility to make connection
attempts to the server, followed by classical username/password cracking
stuff.

The server program is currently programmed to accept a number of
message types from the client program. Each message's data string
starts with a numeric prefix, which indicates the type of message,
followed by a pickled tuple of arguments. The server program reads the
string, extracts the numeric prefix, and passes the rest of the string
to the appropriate function using a subthread.
Ah, okay -- you /do/ already have something running in the middle.
For example, I keep track of who is currently logged in. On startup,
the client connects to my server and sends a '1' followed by their
userid and other information. The server receives this and passed the
data to a 'login' function, which uses a Python dictionary to store the
information. If the server detects that the user is already logged in,
it sends back an error code and the client program displays a message
and terminates. Otherwise it sends back an 'ok' code, and the client
can continue. When the client logs off, it sends a '2' followed by
their userid, which the server receives and passes it to a 'logoff'
function, which deletes the entry from the dictionary.
Obscuring the Python stuff will only be a minor delay factor in
breaking that -- someone really serious could probably stick in a packet
sniffer and record a transaction sequence, eventually reverse mapping
back to the types of operations each code represents.

Database security? First step would be to USE the DBMS privilege
system to limit operations to only those SQL statements, tables, and
data columns that are needed for your client program; since you appear
to be using user/password information already, each such user could have
different privileges, limiting some to retrieval only, for example. As
for your "server", I'd probably start a thread for each connected user,
so that thread handles all communication. Your description sounds more
like a rudimentary proxy adding in a counting scheme, but not really
isolating separate client connections.
-- ================================================== ============ <
wl*****@ix.netcom.com | Wulfraed Dennis Lee Bieber KD6MOG <
wu******@dm.net | Bestiaria Support Staff <
================================================== ============ <
Home Page: <http://www.dm.net/~wulfraed/> <
Overflow Page: <http://wlfraed.home.netcom.com/> <

Sep 12 '05 #8

P: n/a

bruno modulix wrote:
Frank Millman wrote:
Hi all

I am writing a multi-user accounting/business system. Data is stored in
a database (PostgreSQL on Linux, SQL Server on Windows). I have written
a Python program to run on the client, which uses wxPython as a gui,
and connects to the database via TCP/IP.

The client program contains all the authentication and business logic.
It has dawned on me that anyone can bypass this by modifying the
program.
If your program relies on a RDBMS, then it's the RDBMS job to enforce
security rules.


Two possible responses to this -

1. You are right (90% probability)

2. I have certain requirements which can not easily be expressed in the
RDBMS, so it is easier to use the application to enforce certain rules
(10% probability)

Unfortunately I am stuck with number 2 at present.
As it is written in Python, with source available, this would
be quite easy.


Then there's probably something wrong with the way you manage security.


Probably - I am learning the hard way <g>
NB: splitting business logic from the GUI is still a good idea anyway.

I do have it fairly well split, but it all ends up being processed on
the client, which I think is the root of my problem.
--
bruno desthuilliers - unpythonic sig:
python -c "print '@'.join(['.'.join([w[::-1] for w in p.split('.')]) for
p in 'o****@xiludom.gro'.split('@')])"


Thanks

Frank

Sep 12 '05 #9

P: n/a
As a side question Frank, how was your experiences using wxPython for
your GUI?
Any regrets choosing wxPyton over another toolkit?
Was it very buggy?
How was it to work with in general?
Any other real-world wxPython feedback you have is appreciated.

Frank Millman wrote:
I am writing a multi-user accounting/business system. Data is stored in
a database (PostgreSQL on Linux, SQL Server on Windows). I have written
a Python program to run on the client, which uses wxPython as a gui,
and connects to the database via TCP/IP.

<snip>
Sep 12 '05 #10

P: n/a
This is a heck of a can of worms. I've been thinking about these sorts
of things for awhile now. I can't write out a broad, well-structured
advice at the moment, but here are some things that come to mind.

1. Based on your description, don't trust the client. Therefore,
"security", whatever that amounts to, basically has to happen on the
server. The server should be designed with the expectation that any
input is possible, from slightly tweaked variants of the normal
messages to a robotic client that spews the most horrible ill-formed
junk frequently and in large volumes. It is the server's job to decide
what it should do. For example, consider a website that has a form for
users to fill out. The form has javascript, which executes on the
client, that helps to validate the data by refusing to submit the form
unless the user has filled in required fields, etc. This is client-side
validation (analagous to authentication). It is trivial for an attacker
to force the form to submit without filling in required fields. Now if
the server didn't bother to do its own validation but just inserted a
new record into the database with whatever came in from the form
submission, on the assumption that the client-side validation was
sufficient, this would constitute a serious flaw. (If you wonder then
why bother putting in client-side validation at all - two reasons are
that it enhances the user experience and that it reduces the average
load on the server.)

2. If you're moving security and business logic to the server you have
to decide how to implement that. It is possible to rely solely on the
RDBMS e.g., PostgreSQL. This has many consequences for deployment as
well as development. FOr example, if you need to restrict actions based
on user, you will have a different PgSQL user for every business user,
and who is allowed to modify what will be a matter of PgSQL
configuration. The PgSQL is mature and robust and well developed so you
can rely on things to work as you tell them to. On the other hand, you
(and your clients?) must be very knowledgeable about the database
system to control your application. You have to be able to describe
permissions in terms of the database. They have to be able to add new
users to PgSQL for every new business user, and be able to adjust
permissions if those change. You have to write code in the RDBMS
procedural language which, well, I don't know a lot about it but I'm
not to thrilled about the idea. Far more appealing is to write code in
Python. Lots of other stuff.
Imagine in contrast that user authentication is done in Python. In this
scenario, you can have just a single PgSQL user for the application
that has all access, and the Python always uses that database user but
decides internally whether a given action is permitted based on the
business user. Of course in this case you have to come up with your own
security model which I'd imagine isn't trivial. You could also improve
security by combining the approaches, e.g. have 3 database users for 3
different business "roles" with different database permissions, and
then in Python you can decide which role applies to a business user and
use the corresponding database user to send commands to the database.
That could help to mitigate the risks of a flaw in the Python code.

3. You should therefore have a layer of Python that runs on the server
and mediates between client and database. Here you can put
authentication, validation and other security. You can also put all
business logic. It receives all input with the utmost suspicion and
only if everything is in order will it query the database and send
information to the client. There is little or no UI stuff in this
layer. To this end, you should check out Dabo at www.dabodev.com. This
is an exciting Python project that I haven't used much but am really
looking forward to when I have the chance, and as it becomes more
developed. My impression is that it is useable right now. They
basically provide a framework for a lot of stuff you seem to have done
by hand, and it can give you some great ideas about how to structure
your program. You may even decide to port it to Dabo.

Sep 12 '05 #11

P: n/a
On Sep 12, 2005, at 11:26 AM, Frank Millman wrote:
If I move all the authentication and business logic to a program which
runs on the server, it is up to the system administrator to ensure that
only authorised people have read/write/execute privileges on that
program. Clients will have no privileges, not even execute. They will
have their own client program, which has to connect to my server
program, and communicate with it in predefined ways. I *think* that in
this way I can ensure that they cannot do anything outside the bounds
of what I allow them.


I think you have no choice but to do this. Even if you package up the
program in an unmodifiable form, a competent user with a packet sniffer
or even standard OS utilities can determine where you are connecting
and bypass your security/logic. Only if the logic is implemented at a
point beyond the user's reach can you be ensured of logic integrity.

-Michael

Sep 12 '05 #12

P: n/a
Frank Millman a écrit :
bruno modulix wrote:
Frank Millman wrote:
Hi all

I am writing a multi-user accounting/business system. Data is stored in
a database (PostgreSQL on Linux, SQL Server on Windows). I have written
a Python program to run on the client, which uses wxPython as a gui,
and connects to the database via TCP/IP.

The client program contains all the authentication and business logic.
It has dawned on me that anyone can bypass this by modifying the
program.
If your program relies on a RDBMS, then it's the RDBMS job to enforce
security rules.


Two possible responses to this -

1. You are right (90% probability)

2. I have certain requirements which can not easily be expressed in the
RDBMS, so it is easier to use the application to enforce certain rules
(10% probability)


easier, but with a somewhat annoying side-effect... Do you really mean
"easier", or do you think "impossible" ?
Unfortunately I am stuck with number 2 at present.


:-/
As it is written in Python, with source available, this would
be quite easy.


Then there's probably something wrong with the way you manage security.


Probably - I am learning the hard way <g>


As most of us do :-/

Having jumped directly from 2-tiers fat client apps to web apps, I
really have no experience with adding a third tiers to a fat client app,
but AFAICT, Python seems to have a lot to offer here.

BTW, sorry if my answer seemed a bit rude, I didn't mean to be that critic.
Sep 12 '05 #13

P: n/a
On Mon, 12 Sep 2005 06:34:45 -0700, Frank Millman wrote:
The client program contains all the authentication and business logic.
It has dawned on me that anyone can bypass this by modifying the
program. As it is written in Python, with source available, this would
be quite easy. My target market extends well up into the mid-range, but
I do not think that any CFO would contemplate using a program that is
so open to manipulation.


Ha ha ha ha! Oh, you're a funny man! How many CFOs contemplate using
Windows, Internet Explorer, SQL Server, and all the other Microsoft
technologies that are "so open to manipulation" by spyware, viruses
and other malware?

What you do is don't tell them that they can modify the source code. They
won't think of it. And if they do, well, that isn't your problem. That's
an internal problem for their IT department, precisely as it would be if
they gave full read/write permission to everyone in the company instead of
restricting permissions to those who need them.
--
Steven.

Sep 12 '05 #14

P: n/a
Hi,

Why not just releasing the *.pyc ?

Regards,

Philippe


Frank Millman wrote:
Hi all

I am writing a multi-user accounting/business system. Data is stored in
a database (PostgreSQL on Linux, SQL Server on Windows). I have written
a Python program to run on the client, which uses wxPython as a gui,
and connects to the database via TCP/IP.

The client program contains all the authentication and business logic.
It has dawned on me that anyone can bypass this by modifying the
program. As it is written in Python, with source available, this would
be quite easy. My target market extends well up into the mid-range, but
I do not think that any CFO would contemplate using a program that is
so open to manipulation.

The only truly secure solution I can think of would involve a radical
reorganisation of my program, so I am writing to see if anyone has a
simpler suggestion. Here is the idea.

1. Write a socket server program that runs on the server. The socket
server is the only program that connects to the database. The client
program connects to the server, which authenticates the client against
the database and then listens for requests from the client.

2. Devise my own protocol for communication between client and server.
For selects, the client sends a request, the server checks permissions,
then retrieves the data from the database and passes it to the client.
For updates, the client passes up the data to be updated, the server
checks it against its business logic, and then updates the database.

There is the question of where state should be maintained. If on the
server, I would have to keep all the client/server connections open,
and maintain the state of all the sessions, which would put quite a
load on the server. If on the client, I would have to reorganise my
thinking even more, but this would have an advantage - I will
eventually want to write a browser interface, and this would be much
closer in concept, so the two approaches would be quite similar.

This raises the question of whether I should even bother with a gui
client, or bite the bullet and only have a browser based front end.
Judging from recent comments about new technologies such as Ajax, a lot
of the disadvantages have been overcome, so maybe this is the way to
go.

It would be a shame to scrap all the effort I have put into my
wxPython-based front end. On the other hand, it would be pointless to
continue with an approach that is never going to give me what I want.
Any advice which helps to clarify my thinking will be much appreciated.

Thanks

Frank Millman


Sep 12 '05 #15

P: n/a
On Mon, 12 Sep 2005 08:33:10 -0700, Frank Millman wrote:
My problem is that, if someone has access to the network and to a
Python interpreter, they can get hold of a copy of my program and use
it to knock up their own client program that makes a connection to the
database. They can then execute any arbitrary SQL command.


Why is that your problem, instead of the company's problem? It is their
database server, yes? If they want to connect to it and execute arbitrary
SQL commands on their own database, (1) who are you to tell them they
can't? and (2) they hardly need your program to do it.

--
Steven

Sep 12 '05 #16

P: n/a

Steven D'Aprano wrote:
On Mon, 12 Sep 2005 08:33:10 -0700, Frank Millman wrote:
My problem is that, if someone has access to the network and to a
Python interpreter, they can get hold of a copy of my program and use
it to knock up their own client program that makes a connection to the
database. They can then execute any arbitrary SQL command.


Why is that your problem, instead of the company's problem? It is their
database server, yes? If they want to connect to it and execute arbitrary
SQL commands on their own database, (1) who are you to tell them they
can't? and (2) they hardly need your program to do it.

--
Steven


If they choose to give the userid and password to an individual, they
are obviously giving him permission to execute any command.

On the other hand, they can reasonably expect to set up users without
giving them direct access to the database, in which case I think they
would be upset if the users found this restriction easy to bypass.

Frank

Sep 13 '05 #17

P: n/a
Frank Millman wrote:
Steven D'Aprano wrote:
On Mon, 12 Sep 2005 08:33:10 -0700, Frank Millman wrote:
My problem is that, if someone has access to the network and to a
Python interpreter, they can get hold of a copy of my program and use
it to knock up their own client program that makes a connection to the
database. They can then execute any arbitrary SQL command.


Why is that your problem, instead of the company's problem? It is their
database server, yes? If they want to connect to it and execute arbitrary
SQL commands on their own database, (1) who are you to tell them they
can't? and (2) they hardly need your program to do it.

--
Steven


If they choose to give the userid and password to an individual, they
are obviously giving him permission to execute any command.

On the other hand, they can reasonably expect to set up users without
giving them direct access to the database, in which case I think they
would be upset if the users found this restriction easy to bypass.


Certainly, but that access control *shouldn't happen in the client*
whether the source is visible or not.

--
Robert Kern
rk***@ucsd.edu

"In the fields of hell where the grass grows high
Are the graves of dreams allowed to die."
-- Richard Harter

Sep 13 '05 #18

P: n/a
Steve M wrote:
[...]
1. Based on your description, don't trust the client. Therefore,
"security", whatever that amounts to, basically has to happen on the
server.
That's the right answer. Trying to enforce security within your
software running the client machine does not work. Forget the
advice about shipping .py's or packaging the client in some way.
The server should be designed with the expectation that any
input is possible, from slightly tweaked variants of the normal
messages to a robotic client that spews the most horrible ill-formed
junk frequently and in large volumes. It is the server's job to decide
what it should do. For example, consider a website that has a form for
users to fill out. The form has javascript, which executes on the
client, that helps to validate the data by refusing to submit the form
unless the user has filled in required fields, etc. This is client-side
validation (analagous to authentication). It is trivial for an attacker
to force the form to submit without filling in required fields. Now if
the server didn't bother to do its own validation but just inserted a
new record into the database with whatever came in from the form
submission, on the assumption that the client-side validation was
sufficient, this would constitute a serious flaw. (If you wonder then
why bother putting in client-side validation at all - two reasons are
that it enhances the user experience and that it reduces the average
load on the server.)


Good advice.
--
--Bryan
Sep 13 '05 #19

P: n/a
Dennis Lee Bieber wrote:
On 12 Sep 2005 08:33:10 -0700, "Frank Millman" <fr***@chagford.com>
declaimed the following in comp.lang.python:

My problem is that, if someone has access to the network and to a
Python interpreter, they can get hold of a copy of my program and use
it to knock up their own client program that makes a connection to the
database. They can then execute any arbitrary SQL command.
If your DBMS is directly accessible on the net, you're vulnerable
even without Python. Especially if you have "authentication" logic being
done at the client end. There is nothing to prevent someone using a
compatible query browser or command-line utility to make connection
attempts to the server, followed by classical username/password cracking
stuff.


Right - this is the conclusion I have come to.

The server program is currently programmed to accept a number of
message types from the client program. Each message's data string
starts with a numeric prefix, which indicates the type of message,
followed by a pickled tuple of arguments. The server program reads the
string, extracts the numeric prefix, and passes the rest of the string
to the appropriate function using a subthread.

Ah, okay -- you /do/ already have something running in the middle.


It is more on the side than in the middle at present - the client
connects to my server program, but also connects directly to the
database. My proposed change is to put it really in the middle - the
client connects to my server, and my server connects to the database.
For example, I keep track of who is currently logged in. On startup,
the client connects to my server and sends a '1' followed by their
userid and other information. The server receives this and passed the
data to a 'login' function, which uses a Python dictionary to store the
information. If the server detects that the user is already logged in,
it sends back an error code and the client program displays a message
and terminates. Otherwise it sends back an 'ok' code, and the client
can continue. When the client logs off, it sends a '2' followed by
their userid, which the server receives and passes it to a 'logoff'
function, which deletes the entry from the dictionary.

Obscuring the Python stuff will only be a minor delay factor in
breaking that -- someone really serious could probably stick in a packet
sniffer and record a transaction sequence, eventually reverse mapping
back to the types of operations each code represents.


Would using SSL be a solution? This is on my to do list.
Database security? First step would be to USE the DBMS privilege
system to limit operations to only those SQL statements, tables, and
data columns that are needed for your client program; since you appear
to be using user/password information already, each such user could have
different privileges, limiting some to retrieval only, for example. As
for your "server", I'd probably start a thread for each connected user,
so that thread handles all communication. Your description sounds more
like a rudimentary proxy adding in a counting scheme, but not really
isolating separate client connections.
--


A number of replies have indicated that I should be using the DBMS
itself to manage security. I think there are some benefits to managing
it via the application. Admittedly I did not think it through when I
started, but now that I have a reasonable security model working, I
would not want to give it up. Here are some examples of what I can do.

1. Users and groups can be maintained by anyone using my app (with the
correct permissions, of course). You do not have to go through the
database adminstrator with all the complication or red tape that could
arise.

2. I am a great believer in 'field-by-field' validation when doing data
entry, instead of filling in the entire form, submitting it, and then
being informed of all the errors. I can inform a user straight away if
they try to do something they are not entitled to.

3. I can cater for the situation where a user may not have permission
to do something, but they can call a supervisor who can override this.
I have seen solutions which involve prompting for a password, but this
has to be programmed in at every place where it might be required. I
allow the supervisor to enter their userid and password, and my program
reads in their permissions, which become the active ones until
cancelled. I create a flashing red border around the window to warn
them not to forget.

There is some support for this approach given by MS SQL Server, which
has the concept of an Application Role. 'Application roles contain no
users, though you can still assign permissions to application roles.
They are designed to let an application take over the job of
authenticating users from SQL Server.'

Thanks for the interesting comments.

Frank

Sep 13 '05 #20

P: n/a
Bugs wrote:
As a side question Frank, how was your experiences using wxPython for
your GUI?
Any regrets choosing wxPyton over another toolkit?
Was it very buggy?
How was it to work with in general?
Any other real-world wxPython feedback you have is appreciated.

Frank Millman wrote:
I am writing a multi-user accounting/business system. Data is stored in
a database (PostgreSQL on Linux, SQL Server on Windows). I have written
a Python program to run on the client, which uses wxPython as a gui,
and connects to the database via TCP/IP.

<snip>


Difficult to give a balanced answer, but I will try.

wxPython more or less chose itelf. I need the gui to work on Windows
and Linux. Alternatives were Tkinter and PyQt. I got the impression
from reading other comments that Tkinter is a bit old-fashioned and
does not have a native look and feel, and PyQt is not free on Windows.
That left wxPython. I did not evaluate the others, so I cannot compare
directly, but do I have any regrets - no.

Some things that I thought would be difficult I found amazingly easy.
Other things that should have been simple gave me endless trouble.
Understanding the concept of 'sizers' (for laying out the widgets on
the screen) took me a while to grasp, and it is still not 100% clear,
but I can get it to do most of what I want.

The cross-platform capability is very good, but there were times when I
got something to work on one platform and not the other. After much
experimenting I usually managed to get it to work on both, often with a
surprising side-effect - the code I eventually used was often cleaner
and felt more correct than my original attempt, and therefore if I had
been more experienced and done it the 'right' way in the first place, I
may not have had a problem.

Documentation is not perfect, though it is being worked on. The primary
source is the documentation for wxWidgets, which is written in C++.
Some people have commented that they do not understand the format, as
the C++ function calls are not quite the same as Python's, but
personally I did not find this a problem. A bigger problem is that the
documentation does not keep up to date with the product, so there are
often new features available that are not apparent. I have got into the
habit of doing a dir() on most of the objects, to see if they have any
methods that are not listed in the docs - quite often they do. Work has
started on proper wxPython documentation, and apparently it looks quite
good, but I have not used it. There is also a book in the pipeline.

Support from the wxPython community is exceptional. There is a very
willing and helpful mailing list, and a wiki with a lot of useful
stuff. The main developer of wxPython, Robin Dunn, is a regular
contributor to the mailing list, and is the authoritative source of up
to date information. Unfortunately he has been tied up with personal
business for a few months, and his absence from the list is quite
noticeable. I am sure I speak for the entire list when I say I am
really hoping that he returns soon - it makes us realise how dependent
we are on him.

Overall, I have found the experience frustrating from time to time, but
I am happy with what I have achieved. I have shown samples of my app to
a few people, and the appearance has never even raised a question - it
just looks and feels like a modern gui application, and I can get on
with demonstrating the functionality, which is as it should be.

My 2.5c

Frank

Sep 13 '05 #21

P: n/a
Steve M wrote:
This is a heck of a can of worms. I've been thinking about these sorts
of things for awhile now. I can't write out a broad, well-structured
advice at the moment, but here are some things that come to mind.


[snip lots of interesting stuff]

Thanks for the reply, Steve. My thinking has followed a similar path to
yours, so it is good to have some validation.

I have decided that my next step will be to set up a server program as
discussed, and move all my authentication and business logic there,
then get a client program working using the same gui as at present, but
communicating with the server for most of the database stuff instead of
doing it by itself.

I am aware of Dabo, but I have not looked at it yet. I got my own
framework working before Dabo came along, and it works well enough for
my purposes, so I don't really want to go back and reinvent that wheel
at the moment.

Frank

Sep 13 '05 #22

P: n/a
On 13 Sep 2005 01:00:37 -0700, "Frank Millman" <fr***@chagford.com>
declaimed the following in comp.lang.python:
It is more on the side than in the middle at present - the client
connects to my server program, but also connects directly to the
database. My proposed change is to put it really in the middle - the
client connects to my server, and my server connects to the database.
That would be better... The DBMS should never be seen from the
greater net. I don't know how many clients you expect at a time, but I'd
probably use the initial connect request to spin off a connection
specific thread (there's your tracking mechanism -- one thread per "log
in"); you could even build in a time-out mechanism to log-off after
inactivity, something I don't think your simple scheme was rigged to
handle (based on the description).

Would using SSL be a solution? This is on my to do list.
Outside my realm of experience...

1. Users and groups can be maintained by anyone using my app (with the
correct permissions, of course). You do not have to go through the
database adminstrator with all the complication or red tape that could
arise.
I believe it is possible, in MySQL for example, to set up privileges
such that an account can be restricted to an application's database
tables and still be able to add users for just those tables from that
account. At the worst, it should be possible to have a set of
pre-built/parameterized scripts on the server side that can take user
set-up information and create accounts with the proper settings
(validating/ensuring that the accounts only have access to your data and
nothing else). The DBA may have to create the scripts, and then make
them executable from your application's server account, but it would
still be a one-time imposition.
2. I am a great believer in 'field-by-field' validation when doing data
entry, instead of filling in the entire form, submitting it, and then
being informed of all the errors. I can inform a user straight away if
they try to do something they are not entitled to.
Ensuring a field is numeric (with range checking) or string is one
thing... But if a certain user is not even supposed to see a field, or
only have read-only access, those could be determined at the server side
when generating the form (okay, that was phrased more as a web-page
scheme, but...) Better that low-privilege users never even learn of the
additional data fields rather than be tempted by the "forbidden fruit"
<G>
3. I can cater for the situation where a user may not have permission
to do something, but they can call a supervisor who can override this.
I have seen solutions which involve prompting for a password, but this
has to be programmed in at every place where it might be required. I
allow the supervisor to enter their userid and password, and my program
reads in their permissions, which become the active ones until
cancelled. I create a flashing red border around the window to warn
them not to forget.
I suspect such an override could still be done through the server
side. Not sure of the "programmed in at every place" concern -- it
sounds like just an exception handler with retry (and if it were done
via web forms, it would be the server detecting "no privilege" and
returning the override log-in form). I'd be more concerned that the
override doesn't go away after the transaction is completed... To me, if
a lowly user needs to have a supervisor unlock operations -- and the
supervisor then walks away while the higher privileges are active, it is
a sign of either poor security practices, or a need to grant that user
more privileges.

Imagine a store where supervisor approval is needed for any check
over a certain amount... You don't want the supervisor to key in their
approval code on a register and walk away leaving that code active (and
letting the clerk then enter dozens of high value checks without calling
for a supervisor). The code should only be active for the completion of
the check approval and then automatically reset to the regular clerk's
mode of operation.

-- ================================================== ============ <
wl*****@ix.netcom.com | Wulfraed Dennis Lee Bieber KD6MOG <
wu******@dm.net | Bestiaria Support Staff <
================================================== ============ <
Home Page: <http://www.dm.net/~wulfraed/> <
Overflow Page: <http://wlfraed.home.netcom.com/> <

Sep 13 '05 #23

P: n/a
Thanks Frank, I appreciate the feedback.
Sep 13 '05 #24

P: n/a
Frank Millman wrote:
Hi all

I am writing a multi-user accounting/business system. Good!
The client program contains all the authentication and business logic. Not good. You lose...if it's *only* in the client.

Of course, there is no such thing as a safe system, and you need
a pragmatic approach to deciding a level of security.

For a system like this, I'd certainly prefer to design the system
so that it's immune to modifications in any software running on a
machine that isn't "safe", such as a typical end user PC. IOW, no
direct DB access allowed (at least not with anything but select
permissions) and all authentication and security related verifications
etc in a server process which isn't available to ordinary users.

However, such schemes aren't immune to flaws. First of all, there
might be mistakes in the implementations (how ever cleverly they
are made) and there are always people who have access to servers
and databases.

You need audits and checks on various levels to handle such things.
You can probably get transaction logs for the database system to
be stored on some other server, which might make it more difficult
for someone who manipulates the system to hide their tracks. All
external transactions (especially payments from the system) need
to be audited and scanned for irregularities. Can all payments be
traced back to valid business transactions? Are there patterns in
payments that stick out, such as many small transactions to the
same receiver? Security checks like these should probably be made
completely outside your accounting/business software, and by people
who have nothing to do with your main system.

Regardless of your intentions, there is no way you can stop people
from hurting themselves. The customer of the system will basically set
a ceiling for the security of the system, depending on how ambitious
they are, and what kind of infrastructure they can provide etc.
Small customers might well demand perfect security, but they probably
don't want to pay for it.
It has dawned on me that anyone can bypass this by modifying the
program. As it is written in Python, with source available, this would
be quite easy. My target market extends well up into the mid-range, but
I do not think that any CFO would contemplate using a program that is
so open to manipulation.
Just distribute zipped library instead of .py files, and you've raised
the bar to the same level as if you've written it in Java. I.e. it's as
if you send your mail in envelopes (just not glued together) instead of
in postcards. No one will accidentally see the source code, but the
persistent user can.
There is the question of where state should be maintained. If on the
server, I would have to keep all the client/server connections open,
and maintain the state of all the sessions, which would put quite a
load on the server.
Really? How many users do you imagine? How would you plan to organize
your servers? One process per connection, one thread per connection or
asynchronous processing in one thread as in Twisted or asyncore? Perhaps
you should have a look at Twisted ot asyncore?
This raises the question of whether I should even bother with a gui
client, or bite the bullet and only have a browser based front end.
Judging from recent comments about new technologies such as Ajax, a lot
of the disadvantages have been overcome, so maybe this is the way to
go.
It's still a big difference, isn't it? Have you seen a web based
accounting system that you thought was good enough?
It would be a shame to scrap all the effort I have put into my
wxPython-based front end. On the other hand, it would be pointless to
continue with an approach that is never going to give me what I want.
Any advice which helps to clarify my thinking will be much appreciated.


A wxPython client and a Twisted server might well be a useful combo. If
you have well written Python modules for validating data etc, you might
just run them in both client and server to achieve instant feedback on
the client without lowering the security bar.

A budget solution if you want to keep a simple fat client / thin server
solution is to run the clients on a box that the end users can't
manipulate. Just let them access that box via VNC and only run this
client program there. It's not completely trivial to set up such a safe
environment though, but it basically gives you an application server and
very thin clients that still give you the same rich UI as wxPython on
their desktops. They'll have to get used to a non-Windows GUI though,
unless you choose to run Citrix instead of VNC (or use one application
server per user :).

Sep 13 '05 #25

P: n/a
bruno modulix wrote:
Frank Millman wrote:
I am writing a multi-user accounting/business system. Data is stored in
a database (PostgreSQL on Linux, SQL Server on Windows). I have written
a Python program to run on the client, which uses wxPython as a gui,
and connects to the database via TCP/IP.

The client program contains all the authentication and business logic.
It has dawned on me that anyone can bypass this by modifying the
program.


If your program relies on a RDBMS, then it's the RDBMS job to enforce
security rules.


Don't know enough about Millman's app to comment on it
specifically, but many reasonable server-side applications use a
single log-in to the database, then enforce security in the
application server. Web shopping-carts, for example, generally
work that way.
--
--Bryan
Sep 13 '05 #26

P: n/a

Dennis Lee Bieber wrote:
On 13 Sep 2005 01:00:37 -0700, "Frank Millman" <fr***@chagford.com>
declaimed the following in comp.lang.python:

2. I am a great believer in 'field-by-field' validation when doing data
entry, instead of filling in the entire form, submitting it, and then
being informed of all the errors. I can inform a user straight away if
they try to do something they are not entitled to.

Ensuring a field is numeric (with range checking) or string is one
thing... But if a certain user is not even supposed to see a field, or
only have read-only access, those could be determined at the server side
when generating the form (okay, that was phrased more as a web-page
scheme, but...) Better that low-privilege users never even learn of the
additional data fields rather than be tempted by the "forbidden fruit"
<G>


Food for thought - thanks
3. I can cater for the situation where a user may not have permission
to do something, but they can call a supervisor who can override this.
I have seen solutions which involve prompting for a password, but this
has to be programmed in at every place where it might be required. I
allow the supervisor to enter their userid and password, and my program
reads in their permissions, which become the active ones until
cancelled. I create a flashing red border around the window to warn
them not to forget.

I suspect such an override could still be done through the server
side. Not sure of the "programmed in at every place" concern -- it
sounds like just an exception handler with retry (and if it were done
via web forms, it would be the server detecting "no privilege" and
returning the override log-in form). I'd be more concerned that the
override doesn't go away after the transaction is completed... To me, if
a lowly user needs to have a supervisor unlock operations -- and the
supervisor then walks away while the higher privileges are active, it is
a sign of either poor security practices, or a need to grant that user
more privileges.

Imagine a store where supervisor approval is needed for any check
over a certain amount... You don't want the supervisor to key in their
approval code on a register and walk away leaving that code active (and
letting the clerk then enter dozens of high value checks without calling
for a supervisor). The code should only be active for the completion of
the check approval and then automatically reset to the regular clerk's
mode of operation.


H'mm, more food for thought.

The advantage of my approach is that no additional programming is
required. I have a very flexible security model, which is entirely
user-definable. If any user finds that they are blocked from doing
something, they can call anyone who does have that permission, without
exiting from their position in the app. The other person can key in
their userid and password, perform the required action, and the
original user can carry on. You are right that the danger is that the
second person forgets to cancel their code. That is why I create a
flashing red border.

This is how it works from a user perspective. There is a hot-key,
Ctrl-U, which can be pressed at any prompt (wxPython makes this easy).
If pressed, I pop up a box asking for userid and password. If accepted,
I save the original user's permissions, read in the new user's
permissions, and create the flashing border. To cancel the setting, the
user simply presses Ctrl-U again, and everything is reset.

I think this works quite well, so I will run with it for now and see if
it causes any problems in practice.

Many thanks for the valuable comments.

Frank

Sep 14 '05 #27

P: n/a

Magnus Lycka wrote:

[snip lots of interesting stuff]
There is the question of where state should be maintained. If on the
server, I would have to keep all the client/server connections open,
and maintain the state of all the sessions, which would put quite a
load on the server.


Really? How many users do you imagine? How would you plan to organize
your servers? One process per connection, one thread per connection or
asynchronous processing in one thread as in Twisted or asyncore? Perhaps
you should have a look at Twisted ot asyncore?


I was thinking of one thread per connection. How many users? I would be
ecstatic if I got a 20-user system working. However, I know from
experience that these things can mushroom, especially if there are no
licence fees involved, so it can easily be more.

Actually I should just get it working, and then monitor performance as
the number of users increases. I am sure there will be many things I
can do if it starts to slow down.
This raises the question of whether I should even bother with a gui
client, or bite the bullet and only have a browser based front end.
Judging from recent comments about new technologies such as Ajax, a lot
of the disadvantages have been overcome, so maybe this is the way to
go.


It's still a big difference, isn't it? Have you seen a web based
accounting system that you thought was good enough?


Actually no <g>
It would be a shame to scrap all the effort I have put into my
wxPython-based front end. On the other hand, it would be pointless to
continue with an approach that is never going to give me what I want.
Any advice which helps to clarify my thinking will be much appreciated.


A wxPython client and a Twisted server might well be a useful combo. If
you have well written Python modules for validating data etc, you might
just run them in both client and server to achieve instant feedback on
the client without lowering the security bar.


I have seen Twisted mentioned many times in this ng, but I have no idea
what it actually does. Can someone tell me in simple terms what
advantage it might give me over a multi-threaded socket server program.
I have (just now) reread the intro to the asyncore module, and it says
"this strategy can seem strange and complex, especially at first". This
describes my present situation exactly :-)

Many thanks for the valuable comments.

Frank

Sep 14 '05 #28

P: n/a
Frank Millman wrote:
I have seen Twisted mentioned many times in this ng, but I have no idea
what it actually does. Can someone tell me in simple terms what
advantage it might give me over a multi-threaded socket server program.


More control. Less resource usage. Twisted also provides a very
flexible way of building network aware software which you will
appreciate if you ever consider using something else than sockets.

Using several process means using more memory, and inter process
communication isn't as fast as in-process communication. Using
threads is error prone and difficult to debug. There are also scaling
issues with threads in Python (but maybe not when most threads wait
for network connections).

Twisted is based on an event loop, just like GUI programs. But instead
of waiting for GUI events, such as button clicks, the Twisted app will
wait for network events, such as data received. With Twisted and
sockets, you write code that will implement handlers for such events.

A simple example is provided here:

#!/usr/bin/python
# Copyright (c) 2001-2004 Twisted Matrix Laboratories.
# See LICENSE for details.

from twisted.internet.protocol import Protocol, Factory
from twisted.internet import reactor

### Protocol Implementation

# This is just about the simplest possible protocol
class Echo(Protocol):
def dataReceived(self, data):
"""As soon as any data is received, write it back."""
self.transport.write(data)
def main():
f = Factory()
f.protocol = Echo
reactor.listenTCP(8000, f)
reactor.run()

if __name__ == '__main__':
main()

You see? You just subclass Protocol and override the relevant
event handler, and get the thing going! The factory will create
an Echo instance for each socket connection.

The problem in Twisted is that functions in this single thread that
are run inside the event loop must be fast--just as the event
handlers in your GUI app. Twisted helps you achieve this. For
instance, there is the concept of deferred execution, (described
in a paper available at http://python.fyxm.net/pycon/papers/deferex/ )
but you might want to use a separate thread for things like database
queries etc.

There are a lot of other batteries included, check out the Twisted
documentation at
http://twistedmatrix.com/projects/tw...documentation/

You have to ask other Twisted users about scalability, but I think
it will scale well beyond your needs.

Sep 14 '05 #29

P: n/a

Magnus Lycka wrote:
Frank Millman wrote:
I have seen Twisted mentioned many times in this ng, but I have no idea
what it actually does. Can someone tell me in simple terms what
advantage it might give me over a multi-threaded socket server program.


More control. Less resource usage. Twisted also provides a very
flexible way of building network aware software which you will
appreciate if you ever consider using something else than sockets.


[snip Twisted tutorial]

Thanks a ton, Magnus, that really was a brilliant description.

Clearly I must find the time to look into Twisted properly.

Many thanks again

Frank

Sep 14 '05 #30

This discussion thread is closed

Replies have been disabled for this discussion.