Created
November 15, 2018 13:46
-
-
Save Andrew-Lees11/2090aae0cb8e5a0a392d50af2780c39a to your computer and use it in GitHub Desktop.
CouchDB Sessions, Users and Config
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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