Skip to content

Instantly share code, notes, and snippets.

@Andrew-Lees11
Created November 15, 2018 13:46
Show Gist options
  • Save Andrew-Lees11/2090aae0cb8e5a0a392d50af2780c39a to your computer and use it in GitHub Desktop.
Save Andrew-Lees11/2090aae0cb8e5a0a392d50af2780c39a to your computer and use it in GitHub Desktop.
CouchDB Sessions, Users and Config
// CouchDBClient
/// Returns a `UsersDatabase` instance.
public func usersDatabase() -> UsersDatabase {
return UsersDatabase(connProperties: self.connProperties, dbName: "_users")
}
// MARK: Config
/// Set a CouchDB configuration parameter to a new value.
///
/// http://docs.couchdb.org/en/stable/api/server/configuration.html#put--_node-node-name-_config-section-key
/// - parameters:
/// - node: The server node that will be configured.
/// - section: The configuration section to be changed.
/// - key: The key from the configuration section to be changed.
/// - callback: Callback containing an CouchDBError if one occurred.
public func setConfig(node: String = "_local", section: String, key: String, value: String, callback: @escaping (CouchDBError?) -> ()) {
let requestOptions = CouchDBUtils.prepareRequest(connProperties,
method: "PUT",
path: "/_node/\(node)/_config/\(section)/\(key)",
hasBody: true,
contentType: "application/json")
let req = HTTP.request(requestOptions) { response in
if let response = response {
if response.statusCode == .OK {
return callback(nil)
} else {
return callback(CouchDBUtils.getBodyAsError(response))
}
}
return callback(CouchDBError(HTTPStatusCode.internalServerError, reason: "No response from setConfig request"))
}
let jsonValue = "\"" + value + "\""
req.end(jsonValue)
}
/// Get the entire configuration document for a server node.
///
/// http://docs.couchdb.org/en/stable/api/server/configuration.html#node-node-name-config
/// - parameters:
/// - node: The server node with the configuration document.
/// - callback: Callback containing either the configuration dictionary or an CouchDBError if one occurred.
public func getConfig(node: String = "_local", callback: @escaping ([String: [String: String]]?, CouchDBError?) -> ()) {
let requestOptions = CouchDBUtils.prepareRequest(connProperties,
method: "GET",
path: "/_node/\(node)/_config/",
hasBody: false)
CouchDBUtils.couchRequest(options: requestOptions, passStatusCodes: [.OK], callback: callback)
}
/// Get the configuration dictionary for a section of the configuration document.
///
/// http://docs.couchdb.org/en/stable/api/server/configuration.html#node-node-name-config-section
/// - parameters:
/// - node: The server node with the configuration document.
/// - section: The configuration section to be retrieved.
/// - callback: Callback containing either the configuration section dictionary or an CouchDBError if one occurred.
public func getConfig(node: String = "_local", section: String, callback: @escaping ([String: String]?, CouchDBError?) -> ()) {
let requestOptions = CouchDBUtils.prepareRequest(connProperties,
method: "GET",
path: "/_node/\(node)/_config/\(section)",
hasBody: false)
CouchDBUtils.couchRequest(options: requestOptions, passStatusCodes: [.OK], callback: callback)
}
/// Get the value for a specific key in the configuration document.
/// The returned value will be a JSON String.
/// http://docs.couchdb.org/en/stable/api/server/configuration.html#node-node-name-config-section
/// - parameters:
/// - node: The server node with the configuration document.
/// - section: The configuration section to be retrieved.
/// - key: The key in the configuration section for the desired value.
/// - callback: Callback containing either the configuration value as a JSON String or an CouchDBError.
public func getConfig(node: String = "_local", section: String, key: String, callback: @escaping (String?, CouchDBError?) -> ()) {
let requestOptions = CouchDBUtils.prepareRequest(connProperties,
method: "GET",
path: "/_node/\(node)/_config/\(section)/\(key)",
hasBody: false)
let req = HTTP.request(requestOptions) { response in
if let response = response {
// JSONSerialization used here since JSONEncoder will not decode fragments
if let configJSON = CouchDBUtils.getBodyAsData(response),
let jsonString = try? (JSONSerialization.jsonObject(with: configJSON, options: [.allowFragments]) as? String)
{
return callback(jsonString, nil)
} else {
return callback(nil, CouchDBUtils.getBodyAsError(response))
}
} else {
return callback(nil, CouchDBError(HTTPStatusCode.internalServerError, reason: "No response from getConfig request"))
}
}
req.end()
}
// MARK: Sessions
/// Create a new session for the given user credentials.
///
/// - parameters:
/// - name: Username String.
/// - password: Password String.
/// - callback: Callback containing either the session cookie and a `NewSessionResponse`, or an CouchDBError.
public func createSession(name: String, password: String, callback: @escaping (String?, NewSessionResponse?, CouchDBError?) -> ()) {
let requestOptions = CouchDBUtils.prepareRequest(connProperties,
method: "POST",
path: "/_session",
hasBody: true,
contentType: "application/x-www-form-urlencoded")
let body = "name=\(name)&password=\(password)"
let req = HTTP.request(requestOptions) { response in
if let response = response {
if response.statusCode != HTTPStatusCode.OK {
return callback(nil, nil, CouchDBUtils.getBodyAsError(response))
}
do {
let cookie = response.headers["Set-Cookie"]?.first
let newSessionResponse: NewSessionResponse = try CouchDBUtils.getBodyAsCodable(response)
return callback(cookie, newSessionResponse, nil)
} catch {
return callback(nil, nil, CouchDBError(response.httpStatusCode, reason: error.localizedDescription))
}
} else {
return callback(nil, nil, CouchDBError(HTTPStatusCode.internalServerError, reason: "No response from createSession request"))
}
}
req.end(body)
}
/// Verify a session cookie.
///
/// - parameters:
/// - cookie: String session cookie.
/// - callback: Callback containing either the `UserSessionInformation` or an CouchDBError if the cookie is not valid.
public func getSession(cookie: String, callback: @escaping (UserSessionInformation?, CouchDBError?) -> ()) {
var requestOptions: [ClientRequest.Options] = []
requestOptions.append(.hostname(connProperties.host))
requestOptions.append(.port(Int16(connProperties.port)))
requestOptions.append(.method("GET"))
requestOptions.append(.path("/_session"))
var headers = [String : String]()
headers["Accept"] = "application/json"
headers["Content-Type"] = "application/json"
headers["Cookie"] = cookie
requestOptions.append(.headers(headers))
CouchDBUtils.couchRequest(options: requestOptions, passStatusCodes: [.OK], callback: callback)
}
// MARK: Users Database
/// Represents a CouchDB database of users.
public class UsersDatabase: Database {
/// Create new user by name and password.
///
/// - parameters:
/// - name: Username String.
/// - password: Password String.
/// - callback: Callback containing either the DocumentResponse or an NSError.
public func createUser<U: NewUserDocument>(document: U, callback: @escaping (DocumentResponse?, CouchDBError?) -> ()) {
let id = "org.couchdb.user:\(document.name)"
let requestOptions = CouchDBUtils.prepareRequest(connProperties,
method: "PUT",
path: "/_users/\(id)",
hasBody: true,
contentType: "application/json")
print("createUser request options: \(requestOptions)")
CouchDBUtils.documentRequest(document: document, options: requestOptions, callback: callback)
}
/// Get a user by name.
///
/// - parameters:
/// - name: The name of the desired user.
/// - callback: Callback containing either the `RetrievedUserDocument` object, or an NSError.
public func getUser<U: RetrievedUserDocument>(name: String, callback: @escaping (U?, CouchDBError?) -> ()) {
let id = "org.couchdb.user:\(name)"
retrieve(id, callback: callback)
}
}
/// A Default implementation of the `NewUserDocument` protocol with no custom user fields defined.
public struct DefaultNewUserDocument: NewUserDocument {
/// The document ID.
public let _id: String?
/// The document revision.
public var _rev: String?
/// The document type.
public let type: String = "user"
/// The unique immutable username that will be used to log in.
/// If the name already exists, the old user will be replaced with this new user.
public let name: String
/// The password for the user in plaintext.
/// The CouchDB authentication database will replaces this with the secured hash.
public var password: String
/// A list of user roles.
/// CouchDB doesn’t provide any built-in roles, so you define your own depending on your needs.
public var roles: [String]
/// Initialize a `DefaultNewUserDocument`.
///
/// - parameter name: The username that will be used to log in.
/// - parameter password: The password for the user in plaintext.
/// - parameter roles: A list of user roles.
/// - parameter rev: The user revision for updating an existing user.
public init(name: String, password: String, roles: [String], rev: String? = nil) {
self.name = name
self.roles = roles
self.password = password
self._rev = rev
self._id = "org.couchdb.user:" + name
}
}
/// Default implementation of the `RetrievedUserDocument` protocol with no custom user fields defined.
public struct DefaultRetrievedUserDocument: RetrievedUserDocument {
/// The Document ID. Contains user’s name with the prefix "org.couchdb.user:"
public var _id: String?
/// The Document revision.
public var _rev: String?
/// A PBKDF2 key.
public var derived_key: String?
/// User’s name aka login.
public var name: String
/// A list of user roles.
public var roles: [String]
/// Hashed password with salt. Used for "simple" password_scheme.
public var password_sha: String?
/// Password hashing scheme. May be "simple" or "pbkdf2".
public var password_scheme: String
/// Hash salt. Used for "simple" password_scheme.
public var salt: String?
/// Document type. Constantly has the value "user"
public var type: String
}
/// A struct representing the server response when creating a new session.
/// http://docs.couchdb.org/en/stable/api/server/authn.html#post--_session
public struct NewSessionResponse: Codable {
/// Operation status.
public let ok: Bool
/// Username.
public let name: String
/// List of user roles
public let roles: [String]
}
/// A protocol defining the required fields to create a new user document or update an existing user document.
/// http://docs.couchdb.org/en/2.2.0/intro/security.html#creating-a-new-user
public protocol NewUserDocument: Document {
/// The unique immutable username that will be used to log in.
/// If the name already exists, the old user will be replaced with this new user.
var name: String { get }
/// The password for the user in plaintext.
/// The CouchDB authentication database will replaces this with the secured hash.
var password: String { get }
/// A list of user roles.
/// CouchDB doesn’t provide any built-in roles, so you define your own depending on your needs.
var roles: [String] { get }
/// Document type.
var type: String { get }
}
/// A protocol defining the mandatory fields for a user document retrieved from a CouchDB database.
/// http://docs.couchdb.org/en/2.2.0/intro/security.html#users-documents
public protocol RetrievedUserDocument: Document {
/// A PBKDF2 key.
var derived_key: String? { get }
/// User’s name aka login.
var name: String { get }
/// A list of user roles.
/// CouchDB doesn’t provide any built-in roles, so you define your own depending on your needs.
var roles: [String] { get }
/// Hashed password with salt. Used for "simple" password_scheme.
var password_sha: String? { get }
/// Password hashing scheme. May be "simple" or "pbkdf2".
var password_scheme: String { get }
/// Hash salt. Used for "simple" password_scheme.
var salt: String? { get }
/// Document type. Constantly has the value "user"
var type: String { get }
}
/// A struct representing the information about the authenticated user
/// that is returned when a GET request is made to `_session`.
/// http://docs.couchdb.org/en/stable/api/server/authn.html#get--_session
public struct UserSessionInformation: Codable {
/// Operation status.
public let ok: Bool
/// The user context object.
public let userCtx: UserContextObject
/// The session info.
public let info: SessionInfo
/// The users information for the current session.
/// http://docs.couchdb.org/en/stable/json-structure.html#user-context-object
public struct UserContextObject: Codable {
/// Database name.
public let db: String?
/// User name.
public let name: String
/// List of user roles.
public let roles: [String]
}
/// The authentication information about the current session.
public struct SessionInfo: Codable {
/// The authentication method.
public let authenticated: String
/// The database used for authentication.
public let authentication_db: String
/// The Authentication handlers.
public let authentication_handlers: [String]
}
}
import XCTest
import Foundation
@testable import CouchDB
class ConfigTests: CouchDBTest {
static var allTests: [(String, (ConfigTests) -> () throws -> Void)] {
return [
("testConfigTest", testConfigTest)
]
}
func testConfigTest() {
setUpDatabase {
self.delay{self.getNode()}
}
}
func getNode() {
// Get the config node
let requestOptions = CouchDBUtils.prepareRequest((self.couchDBClient?.connProperties)!,
method: "GET",
path: "/_membership",
hasBody: false)
CouchDBUtils.couchRequest(options: requestOptions, passStatusCodes: [.OK]) { (response: [String: [String]]?, error) in
guard let response = response, let node = response["all_nodes"]?[0] else {
return XCTFail("No _membership node response: \(String(describing: error))")
}
print("Found node: \(node)")
self.delay{self.setConfig(node: node)}
}
}
func setConfig(node: String) {
self.couchDBClient?.setConfig(node: node, section: "log", key: "level", value: "debug") { (error) in
if let error = error {
XCTFail("Failed to set config: \(error)")
}
print("Log level set to debug")
self.delay{self.getAllConfig()}
self.delay{self.getConfigSection()}
self.delay{self.getConfigKey()}
}
}
func getAllConfig() {
self.couchDBClient?.getConfig { (config, error) in
if let error = error {
return XCTFail("Failed to get all config: \(error)")
}
let logLevel = config?["log"]?["level"]
XCTAssertEqual(logLevel, "debug")
print("Got all config with debug set")
}
}
func getConfigSection() {
self.couchDBClient?.getConfig(section: "log") { (config, error) in
if let error = error {
return XCTFail("Failed to get config section: \(error)")
}
let logLevel = config?["level"]
XCTAssertEqual(logLevel, "debug")
print("Got config section with debug set")
}
}
func getConfigKey() {
self.couchDBClient?.getConfig(section: "log", key: "level") { (config, error) in
if let error = error {
return XCTFail("Failed to get config section: \(error)")
}
let logLevel = config
XCTAssertEqual(logLevel, "debug")
print("Got config section with debug set")
}
}
}
import XCTest
import Foundation
@testable import CouchDB
class UsersDBTests: CouchDBTest {
static var allTests: [(String, (UsersDBTests) -> () throws -> Void)] {
return [
("testUserDBTest", testUserDBTest)
]
}
func testUserDBTest() {
setUpDatabase {
if let usersDatabase = self.couchDBClient?.usersDatabase() {
self.delay{self.cloudantSecurity(usersDatabase: usersDatabase)}
}
}
}
func cloudantSecurity(usersDatabase: UsersDatabase) {
// Get the config node
let requestOptions = CouchDBUtils.prepareRequest((self.couchDBClient?.connProperties)!,
method: "PUT",
path: "/_security",
hasBody: true)
let body = try! JSONEncoder().encode(["couchdb_auth_only": true])
CouchDBUtils.couchRequest(body: body, options: requestOptions, passStatusCodes: [.OK]) { (response: DocumentResponse?, error) in
if let error = error {
print("Didn't set cloudent security level: \(error)")
}
print("Setting cloudent security: \(String(describing: response))")
self.delay{self.createUser(usersDatabase: usersDatabase)}
}
}
func createUser(usersDatabase: UsersDatabase) {
usersDatabase.getUser(name: "David") { (userDoc: DefaultRetrievedUserDocument?, error) in
if let error = error {
print("get user called with error: \(error)")
} else if let userDoc = userDoc{
print("get user called with existing user: \(userDoc._id ?? ""), \(userDoc._rev ?? ""), \(userDoc.name)")
}
let rev = userDoc?._rev
let newUser = DefaultNewUserDocument(name: "David", password: "password", roles: [], rev: rev)
usersDatabase.createUser(document: newUser) { (response, error) in
if let error = error {
return XCTFail("Failed to create new users: \(error)")
}
XCTAssertTrue(response?.ok ?? false)
usersDatabase.getUser(name: "David") { (userDoc: DefaultRetrievedUserDocument?, error) in
guard let userDoc = userDoc else {
return XCTFail("Failed to create new users: \(String(describing: error))")
}
XCTAssertEqual(userDoc.name, "David")
}
self.delay{self.createSession()}
}
}
}
func createSession() {
couchDBClient?.createSession(name: "David", password: "password") { (cookie, sessionInfo, error) in
guard let cookie = cookie, let sessionInfo = sessionInfo else {
return XCTFail("Did not receive cookie and info when creating session: \(String(describing: error))")
}
print("Created session for \(sessionInfo.name)")
self.delay{self.getSession(cookie: cookie)}
}
}
func getSession(cookie: String) {
couchDBClient?.getSession(cookie: cookie) { (sessionInfo, error) in
guard let sessionInfo = sessionInfo else {
return XCTFail("Failed to get session: \(String(describing: error))")
}
print("Got session for \(sessionInfo.userCtx.name)")
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment