 It's been fun at the coffees and lunches and dinners discovering how many of you work with Django in academia. It's something that I sort of came into by default in academia but in other roles have had to champion and push for. So seeing how many of you have had success getting Django to the enterprise is pretty exciting. So I work at the California College of Arts currently working on a big portal system but before I could build that I had to build a big identity management system and I'll get to what that means exactly in a minute. The slides are at this URL if you want to track them down later. So California, oh yeah, so quick note first. So when I gave this talk at SF Python Meetup one of the pieces of feedback I got was that there was too much code walk through and so I was going to eliminate a lot of code and then other people said well yeah but when you find the slides on Google six months from now you want the code in there at a good point. So what I've done is moved most of the code samples into the end of the slideshow so they're there for future Google searches and I'll just sort of touch on those briefly here. So California College of Arts, two campuses, Oakland and San Francisco, relatively small campus about 2200 students, hundreds of faculty and staff and as a lot of you are probably familiar with IT systems spread across decades and dozens and dozens of systems that all need to talk to each other, some of them modern, some of them legacy, some of them with APIs, some with no APIs, but somehow you've got to glue it all together. And like most campuses we have lots of external and internally hosted web systems, everything from Moodle learning management system to voice thread for collaborating on art projects in real time, simplicity for selecting housing, paper cut for the art students to print their work, media core for sharing large media, Razor's Edge, Vault, web advisor for course selection and one way or another everybody needs to get into these systems, needs to be able to find them and needs to have a central and unified identity. So you know we have to get this right. So you know there's a lot of mission critical weight leaning on this project. So you know it all comes down to LDAP in the end. We had a traditional LDAP server which we recently migrated to Fedora 389. We're also a Google app school so everybody has Google mail and docs and calendar. And then there's the SIS, the student information system which is currently in Datatel from Colleague which is a very old and cranky legacy system which I found a really elegant way to talk to. And then recently the introduction of Workday which is the human resources system and we'll later replace our student information system. So when people log into all of these external systems you never want to go through the process of making them register or sign up for an account. Instead we hook up to something called CAS, centralized authentication service common on campuses. CAS in turns talks to LDAP. So you know as soon as you try to log into Moodle or a WordPress site or whatever you're taken straight off to CAS, CAS checks LDAP. LDAP says you are who you say you are, sends a token back. So you know these systems are set up like whoever LDAP says it's okay go ahead and create an internal account for them here on WordPress or Moodle or whatever it is. So the system that we needed to build needed to by the way is that familiar to anybody? Daniel Johnson just around the block is this mural on the corner of a restaurant called Thai. How are you? So this system we needed to build would do things like activate new student accounts. So you've been accepted by the campus, you're given an ID, now you need to create this account that's going to follow you throughout your campus experience. We also have you know newly hired faculty and staff are coming in through Workday. They need to do the same thing. Staff, people need to change their own passwords. Staffers need to be able to change passwords for people. Your contractors need accounts. We need to set LDAP entitlements so so and so can use the big fancy printer. We have to set Google organizational units because we're using the Google admin API as well. The super users need to be able to edit raw LDAP fields, enabling disabling accounts, email aliases, delegated accounts in Google, LDAP groups, all kinds of crazy stuff and it all had to be done through this one central place. So the experience of this for a student is that they can change the password or they can activate an account and the experience of it for a logged in user is they can change a known password. But the experience for help desk is a whole bunch of powerful tools and utilities and for super users even more of them. Now because when you authenticate through CAS it's going to create a shadow account in that system. That's kind of the CAS standard. You need to be mindful that whatever user names that are going to be created in LDAP need to conform to the lowest common denominator of systems. So while Google may allow a 48 character user name, the old data tel system is not going to allow a user name that. So if you need to shrink it down and same with your diacritical characters and foreign characters. So you need to sort of survey all of your campus systems and say what are the lowest common denominators because that's what we're going to allow into LDAP. Password's not an issue because those are all handled in LDAP. You're so logged in through CAS and we're not going to store a usable password in that system anyway. So just a quick workflow of the process of activation. So this is the Django based system over here. So they're hiring work day. When they come in they're going to verify their account against work day. It says you're verified and that step will select a username and password, create an LDAP account, create a Google account and then meanwhile need to get their newly chosen email back into work day or back into colleague depending on the type of user that it is. So a lot of steps there we need to keep track of and then we have various permission levels. So that's a little bit of what the system ends up looking like. So the activation paths for students versus staff and faculty, they start differently. So students are validated against the student information system, the legacy system, staff and faculty against work day, but they both do the same username and password selection. So two different forms funneling into one shared form that ends up doing all of this stuff. So username, so previously we went with the old first initial last name thing, but the name space is running out. We've been around for 80 years and people want more flexibility, but we don't want to give them infinite flexibility because people can find innumerable ways to create offensive words. So what we wanted to do was if you, you know, said that if your name is Django Reinhart, we would provide you with a prefab list of user names that are guaranteed to exist in LDAP. So I've written some Python code to come up with these variants. And if you have a nickname, we allow you to put that in there as well. So I've been working on Django projects for ever since 0.96, you know, I've worked on a dozen major ones. And the one thing they all have in common and the thing that most of us love about Django is how amazing it is at managing data. And, you know, sort of this philosophy that start with your data modeling, get it right, and everything flows nicely from there. The really big difference with this system is that it didn't store any data internally. It's all about talking to external systems. And so there's the first big Django project I've worked on that really wasn't about internal data management at all. Or just minimal. We use Django's auth system. And then I put dotted lines around these. TMI is a semi exception. I'll talk about it in a minute and the same with logs. But the systems that we're talking to, we're talking to APIs, we're talking XML, we're talking SOAP, doing the CSV shuffle into and out of legacy systems. And because we're getting rid of a lot of the things that Django traditionally does, the ORM, namely, you've got a lot less boiler plate, a lot less stuff you're going to find in the Django docs and you're writing a lot more raw Python. And then we've got these permission tiers. You know, your typical anonymous user and a logged in user and a super user. And then people who are in the group help desk. So this is not the, permissions become interesting for these help desk users because it's not the usual, you know, if user can edit books or if user can create, you know, ISBNs or whatever. Suddenly it's just, we need the permissions that were based on your group membership. And I can't believe I never came hit upon this before. But believe it or not, there is no native Django decorator or template tag to determine if a user is in a group. It seems like basic, I would like to contribute this to Django. I'm going to hopefully talk to some of the committers and see if there's a use case for it because I certainly think there is. So I ended up rolling my own template tags and decorators to determine group membership and handle it accordingly. There are code samples for that and the appendix of this presentation if anybody wants to check them out. They're not that complicated or difficult, but it just seems like the kind of thing you would get by default. So then the question is, well, if you're not using the ORM, you know, which is sort of the core, why use Django at all? What's left over? Well, there's actually lots. You know, there's still the enforcement of clean structures and styles to system architecture. Form validation was huge. I use a lot of form validation and, you know, usually you're working with model forms, but in this case, they were just raw forms with really crazy validation methods that were calling out to other systems and verifying your birthday and, you know, doing the same stuff you always do in form validation except more complicated. And then Django provides, you know, nice URL routing and the templating system. And the session framework I used quite a bit because we've got multiple forms and we're trying to save state between these two forms. And then there's the whole batteries included aspect of Python and the Django ecosystem and being able to pull out all of these common libraries like Django CastNG, which allows a Django app to talk to these Cast servers. So, sys, student information systems, there's lots of them out there and most of them are really old and janky and cranky and impenetrable proprietary systems. Ours is Datatel. It has more than 800 tables and they're mostly poorly designed, no enforcement of schemas, no API, which is really different from, you know, when you create your Django models, it creates a beautifully designed database. So I really, really didn't want to, you know, do everything with CSV shuffles. I wanted to find a way to interact with this legacy system through the ORM. I have one ace in the hole, which is a system called TMI, which stood up a MySQL layer between the student information system. It was read only. So I couldn't write to it, but I could at least get data out of it. So what we ended up doing is, this is your typical multiple database approach. You can see I've got my local Postgres and another one that's defined on some other host as a MySQL server elsewhere. And then there's this wonderful command inspect DB. A lot of you may have seen it, but unless you point your Django instance at an external database and do its best to introspect it and figure out what its Django's models would be, it's not perfect. But it works only on a default database. So you have to temporarily switch your default database, run that management command. And in my case, it was huge. I mean, literally go get some coffee while this thing runs. And I just got this, you know, multi megabyte text dump. And a lot of that is secure sensitive information, which I do not want to risk exposing in my app. So, you know, you don't have to take the whole thing. I was able to just copy and paste bits of that out from the dump file and bring it into my app, not putting the dump file in version control, because we just never want to risk that information getting out. And, you know, once I've just copied and pasted just the basics that I need back in, I'm able to, you know, bring that into my app. And a couple of things to note about this. When you're writing your models, you can actually specify the column that it maps to. So, you know, these are the, you know, it's internal names, but I can use a nice friendly lower case name. And then in the meta, manage equals false. So this tells Django, that's a read-only database I'm talking to. I'm not going to try and migrate it. I'm not going to try and do anything fancy with it. I'm just aware of it. And then, you know, what person it maps to. You can write your own model methods and your own strings on that, et cetera. So, you know, this query doesn't look like very much, but holy crap, I'm able to do ORM queries against this legacy student information system, and that was pretty exciting, and that opened up all kinds of doors for us. So, progress, that's great, except next time I went to run my tests, they crashed and burned. Because this is a little bit of an outside use case for Django, and it doesn't really know what to do when you're talking to read-only databases, because when you run tests, it wants to create a copy of each database on the same database server, and it doesn't have right access to do that. So it turned out to be a number of steps to get around this problem. So, first of all, you want to create a separate settings file that will be just invoked when you're running your tests. So when I run my tests, it's specifying the special test settings file. And in that, I created a separate list of test databases which were similar to the original ones, except the remote one was specified as a local Postgres table. And I had to also tell Django and the test runner that where I've previously defined all those, that external database is unmanaged, now treated as managed. And so the result is that when I run the tests, it looks at that remote read-only MySQL database and instead tests it against a local Postgres copy. I thought that was way too hard, and I wish that Django had helped me more with that. But in the appendix, you'll find an example of that sample test settings file that I used to get that all working. All right, LDAP. I'm not going to go too deep into learning LDAP. It's a pre-arcane language that we're spoiled by working with a Django or about how easy it is to read and write data and deal with relational data. LDAP is not a relational database, and it's got a lot of fancy terminology. There is a library called Python LDAP, which I'll come back to in a second, which simplifies querying LDAP, but it's definitely not the ORM. So it turns out that those calls required quite a bit of code. And I didn't want to litter my views with that code because they weren't really view code. And what I really wanted to focus on was my views handled the request response lifecycle for that user's web interaction, and the interaction with LDAP would happen through an external library. So I ended up writing a PIP installable library with all of my code for creating users and managing groups and managing aliases, et cetera. And then with just one call in the Django view, I could get a true or false from that. So that became interesting, actually, with those functions, like create user, should that return true or false? Or should it return the new object or false? Or should it raise an exception or true? And it was kind of case sensitive. I made different decisions in different parts of that. But this is not finished. It's a work in progress. It also includes similar functions for interacting with the Google APIs. And I was thinking there may be other campuses out there who could use something similar. And I'd like to invite other campuses to collaborate with us on this, and either fork it and do pull requests. Or you may determine that there's stuff in there that's too specific to your campus, or everybody needs something different. So feel free to fork it. But there's the URL. So if you want to join me in this project, please do. It's called CCAUtils. So just an example of the kinds of functions that are in that external library, this is speaking Python LDAP to take a dictionary of properties and create an LDAP user out of it. And then once that's defined and imported from within my Django view, a form is valid. I can just do it as a one-liner. If LDAP create user with a dictionary of arguments, and then just display it and log it. So it really cleaned up my view code. Yeah, so Python LDAP, Lightweight Directory Access Protocol, I sort of had to forget everything I knew about relational databases. I kept hitting these really frustrating points. And this is a great example of them. It's inability to do reverse lookups. So given an LDAP group, I could really easily get a list of its users. But given an LDAP user, there's no way to get a list of the groups that they're in without going through all the groups and iterating them. There is apparently an LDAP plug-in that the system in can configure. But it doesn't come out of the box. And our system wasn't on that for this project. So Python LDAP provides some basic CRUD operations, which this looks very clean and easy. But building these modelists in particular, how to construct those, took a lot of trial and error. And you'll see examples of those in the CCA utils. So we had this talk yesterday from Russell Keith McGee about the new MetaModel interfaces. And wouldn't it be awesome if somebody took that new capability to wrap, did I miss a slide? Whatever. If somebody took that capability and wrapped it so that we could use some ORM-like syntax or the ORM itself to speak to an external LDAP system? Because this is seriously how I felt compared to how I was used to feeling. So I went into the project committed to Python 3. Greenfield's projects should be on Python 3. It turned out that Google's Python client library for the Admin SDK wasn't yet Python 3 ready. And I was really dependent on that. So I was stuck with Python 2. So there is a totally separate LDAP library for Python 3. It's called LDAP 3. The syntax is different. It's not a drop in replacement. Fortunately, Google did update their Python API client library to work with Python 3 right towards the end of my project. And I was out of time and couldn't, so I was frustrated, especially for reasons like this. Because LDAP expects UTF-8 encodings everywhere. And so my code is just littered with these stupid dot encode things. Python 3, one of its big deals is that everything is going to code. And so that would have not been necessary. But maybe next summer we'll see. So just an example of conducting a simple LDAP search. The syntax is not too horrible. So one of the interesting things that came up was that there are a lot of stakeholders in this project, a lot of people who are involved who want to keep track of what's going on. They want to know every action that's taken place in the system that modifies data in any other system in any way, shape, or form. And they want to know who committed that action and at what time and whether it was a success or a failure. And rather than just doing the typical logging, I thought, well, this is kind of a perfect opportunity to use the Django admin. So I built a simple logging utility and a function to call it. So now any stakeholder on the campus can now filter in the admin by LDAP or Google or Workday and by Success or Failure. And they can search for usernames and things. And then there's also a lot of anonymous use of the system for people creating accounts for the first time. So in those cases, the user was a non. But we just trapped the username and logged it as well so we could still see what was going on. I loved how easy it was to write this code. It was just a really, really simple model with a few fields and then this function. And then I can just call log action with a series of attributes, the username and the action message and the success-failure status, et cetera. And so all of that just happens automatically. I then also wrote a management command run by Cron that would query for all log entries marked as failures in the past 24 hours. And I e-mail that out to a bunch of stakeholders every day, or I don't, the system does. And we also have import scripts, so we're doing certain actions that are getting CSV imports and processing those on Cron jobs. And it can also write to the same logging utility. Just a random note, if you need to create passwords, do not try and do it yourself and deal with all the nasty encryption library stuff. Import hashlib to pip install. And it's really as simple as picking your encryption algorithm. And then when you send it into LDAP, the password fields are prefaced by the algorithm that's in use. Makes it so easy, and you don't have to worry about it. So all of that seems pretty clean, but that's not my desktop, by the way. There are certain aspects of the system that no matter how much I tried to clean them up and simplify the views. And like anyone, don't want long, crazy functions. But in the case of ActivateUser, once that form is valid, a whole bunch of things have to happen in sequence, and it needs to log all those things. So you've got different types of users starting the activation differently. We're saving state between the forms. Some users have nicknames. We're displaying success and error messages differently to different types of users, you know, sysadmins versus end users, trying to build the data objects correctly so that LDAP will accept them, hashing the passwords correctly, adding the right users of the right groups and entitlements, and then going out and creating a Google user in the same step. So I'm ashamed to say that I've got now a 200 line function in there, ActivateStep2. But I can't figure out any way to make it shorter, because it all has to happen at once. OK, talking to Google. So there's the other piece of this, is that for every LDAP account, there needs to be a matching Google account in their directory. So for this, you use Google's admin SDK, aka the Directory API. This applies a Python client library. Every action has to be done by what's known as a service user, the special user you create in Google and give access to. And it also has to be referenced by a subuser, which is a human on your team who is ultimately responsible for that change. So this would be like a sysadmins email address or something. So these calls have to actually invoke both of them. And I got to learn about two-legged OAuth versus three-legged OAuth. So three-legged is the kind you're used to seeing where Facebook would like to do such and such on your behalf, and is this OK with you? It's running a dialog in the user's screen. In this case, we want our system to interact with Google transparently to the user. They never need to know about it. So that's two-legged OAuth. It's just between the two Google and our system, and the user doesn't know anything about it. So basically the way that's set up, and I won't go into all the detail here, but you need to look in their APIs, and every API has what's called a scope. And the scope is URL that refers to some capability. So if I want to get a group, I discover that it's this scope. And I capture that. And then step two is you go into the Google Apps admin console, dig way deep into security advanced API client access, and you associate the service account with that scope. And that gives that service account permission to execute on that scope. And then I'm able to write these reusable functions. And just like the LVAP ones, these live off in our reusable, pip installable, CCAUtils library. So I've got a reusable get-off, which involves opening the key file they provided, and signing it with the client email, and the private key, and that scope, and then that sub-user I mentioned, and then finally returning that off handle, which then can be utilized by second functions, such as building a service. So here's updating the user record at Google. And after you go through this step of getting the off, you build a service. So this is the alternative to working with a restful URL. So you build a service on the Admin SDK, the API version directory v1. And once I've got a handle on that service, service.users.update with the key, that'd be the email. So that's the email of the user we're operating on. And then the body would be a dictionary of data, and you can execute it all at once. So whether that's easier than using the rest API, I'm not sure, but once I got it down, it was pretty straightforward. And then once all that exists, then it becomes a one liner in my view. So I'm not littering all my view code with all that stuff. And all I have to do is build a message string, and display it to screen, and log it to the logging system. So then we had this need to set up email delegates. So Google has this cool system where people can access another account without entering the password for it. So I can say Joe is a delegate for Mary, and Joe can access Mary's email with his own password. Unfortunately, that's not part of the Admin SDK. It's part of their email SDK, which hasn't been updated to these newer systems. And so it was a whole other API exploration. Hopefully it's not service-based. I have some sample code for that. If anybody needs it, it returns XML instead of JSON. So now we're in a beautiful soup land. This stuff's just got hairy fast. All right, more fun. So rethinking passwords, we got really frustrated with the morass of password advice that's out there. Because end users are getting contradictory password advice from every system they know. One system in is saying, random, random, random. It has to be random and long. And others are saying correct horse battery staple. You can use plain English words as long and unguessable. And others are saying, well, if it's short and random, that's better. And you've got systems saying, well, you have to have an uppercase and a lowercase and a punctuation mark. What we care to do, oh, and people need to be able to type them into a mobile device. So people have different desires in terms of creating a strong password. And we thought the rules were in the way of good passphrases. But you know what? We didn't care. We're just like the honey badger. We don't care how you got there. So if you want to do an eight character fully random password that is considered strong, that's great. Or if you want to do correct goat battery staple, that's cool too. The problem is that now you're outside of rules. And so now how do you do your form validation? And it turned out to be pretty difficult with different sets of rules. And I started looking into this problem. And I discovered that Dropbox actually created and open sourced a solution to just this. It's called ZXCVBN for the lower left row of your keyboard. And this is actually what they use in production at Dropbox. But it measures password strength as a matter of what they call entropy. But essentially you just set a strength threshold. And it takes a whole bunch of things into account. It does crazy dictionary lookups. It lets you penalize certain strings. So I was able to penalize the name of the college, penalize your own username, penalize examples I had given on the password help page, and let people create passwords however they want. So the issue with that is that it's JavaScript based. And if it's JavaScript based, that means dictionary lookups are hard because you can't be transmitting dictionaries over the wire. But we wanted the really rich interactions. We wanted a wizzy strength bar that would go up and down as you typed in real time. So somebody ported this to Python. That was excellent. Now I could do it on the back end, but then I would lose the rich JavaScript goodness. So what I ended up doing was using it on the back end, making a simple JSON endpoint that you could call with any given password string. And it would return zxdvbn's whole dictionary of attributes and penalties and ultimate strength. And then I ran it through jQuery to bounce because we want to have pretty real time interactions. But I don't want to call that API every time you touch a key on the keyboard. So I use jQuery to bounce to wait 250 milliseconds after you stop typing. And then it would call the send point. And this thing would go up and down. I was going to do a live demo, but we've got some screen sharing issues here. But this does exactly what I was saying. You can have eight characters of totally random, or you can have nice long, memorable Sergeant Peppers, Mr. Kite type passwords, whatever you like. I ended up doing a big blog post. So all the sample code for this system is at that URL, which you can get out of the slides later if you're interested in a system like that. So Workday. Workday is this modern, currently mostly human resources and payment and scheduling system. They are also introducing a student information system to it for the future. And other pieces will come along. We are one of three pilot schools out there who are going to be experimenting with their student information systems. But we did move all of our hiring and payment stuff to them. Because I have to validate a user against Workday, I had to talk to their APIs. And quickly hit some roadblocks. It got frustrating. Because in my world, in our Python, Django, modern web application development world, it's all about REST. Now APIs are all about REST. But they're very much in the Java, Windows, heavyweight system world. And most of their stuff is soap-based. They claim to have a REST API, but it doesn't do everything. So after trying and failing to get what I needed out of their system with REST, I was thrown back on soap, only to find that it's considered so sort of deprecated in our modern world that there's even SUDS, which is the most famous soap library, has been deprecated and replaced by SUDS, Djerco. And I couldn't find a single person online who was interacting with Workday and soap. So it took a fair bit of experimentation to get that working, but I did. And I wrote it up in a gist. If anybody is out there and needs to do this, there's a link to how we ended up solving that. So that's what I've got for today. And yeah, then the code samples are here if anybody wants to see them. Or we can open it up for questions. Any questions? Knowing there's a lot of universities out there and knowing what you know now, like what would you, somebody asked you to do this again. What would you do differently having gone through this huge project? Right. Differently? Well, I mean, I had the advantage of time. So I would have been able to start with Python 3. So that would have saved a lot of frustration. But if I had to do it back when I did it, then I still would not have had that option. I mean, I learned a lot in the process. It would have gone a lot quicker. I would have had a lot more sample code to work with. But yeah, I think my biggest mistake was not communicating enough with some of the legacy data teams about certain ways they wanted to interact with the student information system. So communication is probably the biggest thing. Hi. You talked about the least common denominator for when you're trying to design it. But what if that changes in the middle? You need to interact with another system. Which seems to have a max length of five for a user name. Contrived example, but I mean. Right. I mean, it's a case by case basis. The one issue that we hit there that we weren't able to anticipate was these Canon copiers and their fax capability. So somebody could go in fax from a copier. And it turns out that they didn't like dots in user names. Are you kidding me? Or in email addresses, right? So we were able to update firmware on those to fix that. Another system that suddenly had a much lower. I mean, we try to fix that system, right? We're trying to move into the next era here and hopefully not be dragged back by the legacy of the past. But if you're forced to, we might have to limit user names to 12 characters for everybody or whatever if it really depends on the system and whether it could be upgraded. Thank you. You mentioned that you stored some information in this session. And I was wondering if you ever ran into conflicts where your Django session was getting out of parity with another systems session or anything like that? Nothing there. The only few issues that I had had to do with the help desk users and the super users who were doing things like helping users reset passwords over and over again. And so you'd have a session variable leftover that was interfering. So those were bugs that I needed to clear out just that session variable. If I understand it right, something changed in Django, I think in 1.8, because my first approach on that was just to kill all session variables. That turns out now, it didn't used to, but now it will actually log you out of the Django app you logged into. So that turned out to be a not viable solution. I actually had to actually delete session variables one key at a time, but I haven't had any conflicts with other systems on campus now. I've used Python LDAP a lot. And I've gone back and forth in my apps. Like I've tried to make it so that we're just using their Active Directory groups for all of our auth, like authorization and permissions. I guess, yeah, authentication and authorization. And then I've ended up having to make some things really quick where I actually end up using the Django, the permissions, and actually putting that in the database. Do you have opinions on that? I mean, have you had to do that? And do you have a preference or is there? I haven't had to and I would strongly resist it. The system that I'm building is not a canonical data store. It's a canonical place to make these kinds of changes, but it needs to be making these changes out in external systems, not within itself. So no other system is going to look to my system saying, what permissions does such and such user have? We eventually will bring in grouper into LDAP and that will make it even more in LDAP land and not in my space. So yeah, I'm trying to store as little data as possible. Really just the logging data is the only thing, yeah. OK, I've literally never done that this is more of a comment than a question. But I'm really excited because I presented yesterday on Project Callisto, which is a system for sexual assault reporting college campuses. And we're using your implementation of ZXC-VVN. And it's really, it was super helpful. I love the walkthrough. And so yeah, it's really cool. That's awesome. Thank you. I'm very happy to hear that. Thank you. Yay. Yes, I had another comment as well. It looked like in the example where you were building an LDAP filter string, you're using Python string substitution. But actually, there's an LDAP string substitution that will sort of in the same way that database string substitution will prevent. Code execution. So what's the advantage of doing it that way? So if you have characters that you're trying to substitute in, you won't be able to sort of break out of your LDAP filter and it'll escape characters, yeah. So we have a security advantage. Thanks for that tip. I didn't know that. I was writing Python. And so I wrote it Pythonically. But I'll have a look at that, yeah. You talked a little bit about testing. I was wondering what you had in place for not only unit tests, but mostly in terms of functional unit tests with, did they actually talk to the legacy systems? Oh, yeah, good question. So I mean, there's sort of several levels of that. There's the CCAutils, which has the modules for talking to LDAP and Google. And those have very basic unit tests. And I would mock data and I have a test user that runs for a lot of that stuff. And then there's the Django side. And that's where most of the functional tests are. So I'm testing entire views and things. But is your question about the risk of modifying data in a live LDAP system? So in my case, I'm restricting it very carefully to a single designated, what I call, direct test user. It would be great to have a secondary LDAP system that I could test against, just like you have a test database. I'd feel better about that. But it's not feasible in our case. Any other questions? So we also have something at work. This is slightly not related to LDAP, exactly. But when you talked about testing, you guys spin up your own like an additional DB to do the testing against. And so what we do at work is just like work around right now, and it's something we're going to solve probably next week. I don't know. But Django 1.8 comes with a keep DB flag. And if we want to run tests locally, like on my system, then we run it against our local DB. We clear it out first. But can you talk a little bit about what you had to do exactly? Like you had a custom manager to work around to pull up your own DB. Is this something that is just, if you could just talk about what exactly you do then? Yeah, I mean, I do it because I have to. There'd be no other way to run tests. As long as you're defining two databases, it's going to try and create a test version of that database on that same host by default. So I basically literally could not run tests unless I'd gone through this process. As for keep DB, I think the point of that is really to speed up your tests, right? You know your situation well enough to know that there's no downside to keeping the same test database between test runs. I think what we do is we actually just, at least on my local dev machine, not CI, but we use an instance of the DB to develop with. And I just run my tests on that with keep DB. This way, I don't have to pull up a separate DB for testing, which is far from ideal. Please don't ever do that. The advantage is we have a lot of custom SQL that we need to get those tables up and running, which we use in production and we use a mirror of that. So this is the advantage that we get, because we also use the managed false flags and we're looking onto a system that we don't define the models for. So I don't know if this is, like it seems like a forest strategy because any test data which conflicts with your dev data can cause issues, but yeah, I don't know. So that's an alternative to constructing a copy of a remote MySQL database as a local Postgres database. You can just use it as keep DB all the time. So yeah, I guess I'd be concerned about the integrity of the tests, because now you don't have 100% guarantee that the test database data that you think you're testing is actually the data you're testing, or do you? I mean, I'm not sure. Maybe this seems like it's gonna be a longer conversation if I'm allowed to. Okay, maybe you can show me some code. Yeah, sure. Yeah, I did come across keep DB as a tip for speeding things up, but didn't really. I came across it by writing it, but I missed it. And then I just realized that Django had one, so I'm... There you go. All right, okay. Awesome, thank you. Thank you, Scott. All right, thanks.