diff --git a/Guides/Development.md b/Guides/Development.md index 0f7b0dacb..0269f89d5 100644 --- a/Guides/Development.md +++ b/Guides/Development.md @@ -1,7 +1,7 @@ # Swift Driver Development Guide ## Index -* [Things to Install](#things-to-install) +* [Things to Install](#things-to-install) * [The Code](#the-code) * [Building](#building) * [Running Tests](#running-tests) @@ -15,15 +15,17 @@ * [swiftenv](https://swiftenv.fuller.li/en/latest/installation.html): a command-line tool that allows easy installation of and switching between versions of Swift. * [Jazzy](https://github.com/realm/jazzy#installation): the tool we use to generate documentation. * [SwiftFormat](https://github.com/nicklockwood/SwiftFormat#how-do-i-install-it): the Swift formatter we use. -* [SwiftLint](https://github.com/realm/SwiftLint#using-homebrew): the Swift linter we use. +* [SwiftLint](https://github.com/realm/SwiftLint#using-homebrew): the Swift linter we use. * [Sourcery](https://github.com/krzysztofzablocki/Sourcery/#installation): the tool we use for code generation. ## The code You should clone this repository, as well as the [MongoDB Driver specifications](https://github.com/mongodb/specifications). -## Building +## Building ### From the Command line -Run `swift build` or simply `make` in the project's root directory. +Run `swift build` or simply `make` in the project's root directory. + +If you add symbols you may need to run `make exports` which will generate [Sources/MongoSwiftSync/Exports.swift](Sources/MongoSwiftSync/Exports.swift). This makes symbols declared in `MongoSwift` available to importers of `MongoSwiftSync`. ### In Xcode We do not provide or maintain an already-generated `.xcodeproj` in our repository. Instead, you must generate it locally. @@ -33,21 +35,21 @@ We do not provide or maintain an already-generated `.xcodeproj` in our repositor 2. Run `make project` 3. You're ready to go! Open `MongoSwift.xcodeproj` and build and test as normal. -Why is this necessary? The project requires a customized "copy resources" build phase to include various test `.json` files. By default, this phase is not included when you run `swift package generate-xcodeproj`. So `make project` first generates the project, and then uses `xcodeproj` to manually add the files to the appropriate targets (see `add_json_files.rb`). +Why is this necessary? The project requires a customized "copy resources" build phase to include various test `.json` files. By default, this phase is not included when you run `swift package generate-xcodeproj`. So `make project` first generates the project, and then uses `xcodeproj` to manually add the files to the appropriate targets (see `add_json_files.rb`). ## Running Tests **NOTE**: Several of the tests require a mongod instance to be running on the default host/port, `localhost:27017`. You can start this by running `mongod --setParameter enableTestCommands=1`. The `enableTestCommands` parameter is required to use some test-only commands built into MongoDB that we utilize in our tests, e.g. `failCommand`. You can run tests from Xcode as usual. If you prefer to test from the command line, keep reading. -### From the Command Line +### From the Command Line We recommend installing the ruby gem `xcpretty` and running tests by executing `make test-pretty`, as this provides output in a much more readable format. (Works on MacOS only.) Alternatively, you can just run the tests with `swift test`, or `make test`. To filter tests by regular expression: -- If you are using `swift test`, provide the `--filter` argument: for example, `swift test --filter=MongoClientTests`. -- If you are using `make test` or `make test-pretty`, provide the `FILTER` environment variable: for example, `make test-pretty FILTER=MongoCollectionTests`. +- If you are using `swift test`, provide the `--filter` argument: for example, `swift test --filter=MongoClientTests`. +- If you are using `make test` or `make test-pretty`, provide the `FILTER` environment variable: for example, `make test-pretty FILTER=MongoCollectionTests`. ### Diagnosing Backtraces on Linux @@ -61,10 +63,10 @@ $ symbolicate-linux-fatal /path/to/MongoSwiftPackageTests.xctest crash.log This will require you to manually provide the path to the compiled test binary (e.g. `.build/x86_64-unknown-linux/debug/MongoSwiftPackageTests.xctest`). ## Writing and Generating Documentation -We document new code as we write it. We use C-style documentation blocks (`/** ... */`) for documentation longer than 3 lines, and triple-slash (`///`) for shorter documentation. +We document new code as we write it. We use C-style documentation blocks (`/** ... */`) for documentation longer than 3 lines, and triple-slash (`///`) for shorter documentation. Comments that are _not_ documentation should use two slashes (`//`). -Documentation comments should generally be complete sentences and should end with periods. +Documentation comments should generally be complete sentences and should end with periods. Our documentation site is automatically generated from the source code using [Jazzy](https://github.com/realm/jazzy#installation). We regenerate it via our release script each time we release a new version of the driver. @@ -93,7 +95,7 @@ If you have a setup for developing the driver in an editor other than the ones l * Please read the [NOTICE](https://github.com/Utagai/swift.vim#notice) for proper credits. ## Workflow -1. Create a feature branch, named by the corresponding JIRA ticket if exists, along with a short descriptor of the work: for example, `SWIFT-30/writeconcern`. +1. Create a feature branch, named by the corresponding JIRA ticket if exists, along with a short descriptor of the work: for example, `SWIFT-30/writeconcern`. 1. Do your work on the branch. 1. If you add, remove, or rename any tests, make sure to update `LinuxMain.swift` accordingly. If you are on MacOS, you can do that by running `make linuxmain`. 1. Ensure your code passes both the linter and the formatter. @@ -103,8 +105,8 @@ If you have a setup for developing the driver in an editor other than the ones l 1. Open a pull request on the repository. Make sure you have rebased your branch onto the latest commits on `master`. 1. Go through code review to get the team's approval on your changes. (See the next section on [Code Review](#code-review) for more details on this process.) Once you get the required approvals and your code passes all tests: 1. Rebase on master again if needed. -1. Rerun tests. -1. Squash all commits into a single, descriptive commit method, formatted as: `TICKET-NUMBER: Description of changes`. For example, `SWIFT-30: Implement WriteConcern type`. +1. Rerun tests. +1. Squash all commits into a single, descriptive commit method, formatted as: `TICKET-NUMBER: Description of changes`. For example, `SWIFT-30: Implement WriteConcern type`. 1. Merge it, or if you don't have permissions, ask someone to merge it for you. ## Code Review diff --git a/Sources/MongoSwift/MongoClient.swift b/Sources/MongoSwift/MongoClient.swift index 7268b381f..3106ccc02 100644 --- a/Sources/MongoSwift/MongoClient.swift +++ b/Sources/MongoSwift/MongoClient.swift @@ -342,6 +342,7 @@ public class MongoClient { * - Parameters: * - filter: Optional `Document` specifying a filter that the listed databases must pass. This filter can be based * on the "name", "sizeOnDisk", "empty", or "shards" fields of the output. + * - options: Optional `ListDatabasesOptions` specifying options for listing databases. * - session: Optional `ClientSession` to use when executing this command. * * - Returns: @@ -352,14 +353,16 @@ public class MongoClient { * - `LogicError` if the provided session is inactive. * - `LogicError` if this client has already been closed. * - `EncodingError` if an error is encountered while encoding the options to BSON. + * - `CommandError` if options.authorizedDatabases is false and the user does not have listDatabases permissions. * * - SeeAlso: https://docs.mongodb.com/manual/reference/command/listDatabases/ */ public func listDatabases( _ filter: Document? = nil, + options: ListDatabasesOptions? = nil, session: ClientSession? = nil ) -> EventLoopFuture<[DatabaseSpecification]> { - let operation = ListDatabasesOperation(client: self, filter: filter, nameOnly: nil) + let operation = ListDatabasesOperation(client: self, filter: filter, nameOnly: nil, options: options) return self.operationExecutor.execute(operation, client: self, session: session).flatMapThrowing { result in guard case let .specs(dbs) = result else { throw InternalError(message: "Invalid result") @@ -373,6 +376,7 @@ public class MongoClient { * * - Parameters: * - filter: Optional `Document` specifying a filter on the names of the returned databases. + * - options: Optional `ListDatabasesOptions` specifying options for listing databases. * - session: Optional `ClientSession` to use when executing this command * * - Returns: @@ -382,12 +386,14 @@ public class MongoClient { * If the future fails, the error is likely one of the following: * - `LogicError` if the provided session is inactive. * - `LogicError` if this client has already been closed. + * - `CommandError` if options.authorizedDatabases is false and the user does not have listDatabases permissions. */ public func listMongoDatabases( _ filter: Document? = nil, + options: ListDatabasesOptions? = nil, session: ClientSession? = nil ) -> EventLoopFuture<[MongoDatabase]> { - self.listDatabaseNames(filter, session: session).map { $0.map { self.db($0) } } + self.listDatabaseNames(filter, options: options, session: session).map { $0.map { self.db($0) } } } /** @@ -395,6 +401,7 @@ public class MongoClient { * * - Parameters: * - filter: Optional `Document` specifying a filter on the names of the returned databases. + * - options: Optional `ListDatabasesOptions` specifying options for listing databases. * - session: Optional `ClientSession` to use when executing this command * * - Returns: @@ -404,12 +411,14 @@ public class MongoClient { * If the future fails, the error is likely one of the following: * - `LogicError` if the provided session is inactive. * - `LogicError` if this client has already been closed. + * - `CommandError` if options.authorizedDatabases is false and the user does not have listDatabases permissions. */ public func listDatabaseNames( _ filter: Document? = nil, + options: ListDatabasesOptions? = nil, session: ClientSession? = nil ) -> EventLoopFuture<[String]> { - let operation = ListDatabasesOperation(client: self, filter: filter, nameOnly: true) + let operation = ListDatabasesOperation(client: self, filter: filter, nameOnly: true, options: options) return self.operationExecutor.execute(operation, client: self, session: session).flatMapThrowing { result in guard case let .names(names) = result else { throw InternalError(message: "Invalid result") diff --git a/Sources/MongoSwift/Operations/ListDatabasesOperation.swift b/Sources/MongoSwift/Operations/ListDatabasesOperation.swift index 66e2a63f7..d33098375 100644 --- a/Sources/MongoSwift/Operations/ListDatabasesOperation.swift +++ b/Sources/MongoSwift/Operations/ListDatabasesOperation.swift @@ -25,20 +25,34 @@ internal enum ListDatabasesResults { case names([String]) } +/// Options for "listDatabases" operations. +public struct ListDatabasesOptions { + /// Specifies whether to only return databases for which the user has privileges. + public var authorizedDatabases: Bool? + + /// Convenience initializer allowing any/all parameters to be omitted or optional. + public init(authorizedDatabases: Bool? = nil) { + self.authorizedDatabases = authorizedDatabases + } +} + /// An operation corresponding to a "listDatabases" command on a collection. internal struct ListDatabasesOperation: Operation { private let client: MongoClient private let filter: Document? private let nameOnly: Bool? + private let options: ListDatabasesOptions? internal init( client: MongoClient, filter: Document?, - nameOnly: Bool? + nameOnly: Bool?, + options: ListDatabasesOptions? ) { self.client = client self.filter = filter self.nameOnly = nameOnly + self.options = options } internal func execute(using connection: Connection, session: ClientSession?) throws -> ListDatabasesResults { @@ -51,6 +65,9 @@ internal struct ListDatabasesOperation: Operation { if let nameOnly = self.nameOnly { cmd["nameOnly"] = .bool(nameOnly) } + if let authorizedDatabases = self.options?.authorizedDatabases { + cmd["authorizedDatabases"] = .bool(authorizedDatabases) + } let opts = try encodeOptions(options: nil as Document?, session: session) var reply = Document() diff --git a/Sources/MongoSwiftSync/Exports.swift b/Sources/MongoSwiftSync/Exports.swift index ecbf3f3bb..225e47179 100644 --- a/Sources/MongoSwiftSync/Exports.swift +++ b/Sources/MongoSwiftSync/Exports.swift @@ -75,6 +75,7 @@ @_exported import struct MongoSwift.InternalError @_exported import struct MongoSwift.InvalidArgumentError @_exported import struct MongoSwift.ListCollectionsOptions +@_exported import struct MongoSwift.ListDatabasesOptions @_exported import struct MongoSwift.LogicError @_exported import struct MongoSwift.MongoNamespace @_exported import struct MongoSwift.ObjectId diff --git a/Sources/MongoSwiftSync/MongoClient.swift b/Sources/MongoSwiftSync/MongoClient.swift index 978827bea..69aa64387 100644 --- a/Sources/MongoSwiftSync/MongoClient.swift +++ b/Sources/MongoSwiftSync/MongoClient.swift @@ -92,6 +92,7 @@ public class MongoClient { * - Parameters: * - filter: Optional `Document` specifying a filter that the listed databases must pass. This filter can be based * on the "name", "sizeOnDisk", "empty", or "shards" fields of the output. + * - options: Optional `ListDatabasesOptions` specifying options for listing databases. * - session: Optional `ClientSession` to use when executing this command. * * - Returns: A `[DatabaseSpecification]` containing the databases matching provided criteria. @@ -99,14 +100,16 @@ public class MongoClient { * - Throws: * - `LogicError` if the provided session is inactive. * - `EncodingError` if an error is encountered while encoding the options to BSON. + * - `CommandError` if options.authorizedDatabases is false and the user does not have listDatabases permissions. * * - SeeAlso: https://docs.mongodb.com/manual/reference/command/listDatabases/ */ public func listDatabases( _ filter: Document? = nil, + options: ListDatabasesOptions? = nil, session: ClientSession? = nil ) throws -> [DatabaseSpecification] { - try self.asyncClient.listDatabases(filter, session: session?.asyncSession).wait() + try self.asyncClient.listDatabases(filter, options: options, session: session?.asyncSession).wait() } /** @@ -114,18 +117,21 @@ public class MongoClient { * * - Parameters: * - filter: Optional `Document` specifying a filter on the names of the returned databases. + * - options: Optional `ListDatabasesOptions` specifying options for listing databases. * - session: Optional `ClientSession` to use when executing this command * * - Returns: An Array of `MongoDatabase`s that match the provided filter. * * - Throws: * - `LogicError` if the provided session is inactive. + * - `CommandError` if options.authorizedDatabases is false and the user does not have listDatabases permissions. */ public func listMongoDatabases( _ filter: Document? = nil, + options: ListDatabasesOptions? = nil, session: ClientSession? = nil ) throws -> [MongoDatabase] { - try self.listDatabaseNames(filter, session: session).map { self.db($0) } + try self.listDatabaseNames(filter, options: options, session: session).map { self.db($0) } } /** @@ -133,15 +139,21 @@ public class MongoClient { * * - Parameters: * - filter: Optional `Document` specifying a filter on the names of the returned databases. + * - options: Optional `ListDatabasesOptions` specifying options for listing databases. * - session: Optional `ClientSession` to use when executing this command * * - Returns: A `[String]` containing names of databases that match the provided filter. * * - Throws: * - `LogicError` if the provided session is inactive. + * - `CommandError` if options.authorizedDatabases is false and the user does not have listDatabases permissions. */ - public func listDatabaseNames(_ filter: Document? = nil, session: ClientSession? = nil) throws -> [String] { - try self.asyncClient.listDatabaseNames(filter, session: session?.asyncSession).wait() + public func listDatabaseNames( + _ filter: Document? = nil, + options: ListDatabasesOptions? = nil, + session: ClientSession? = nil + ) throws -> [String] { + try self.asyncClient.listDatabaseNames(filter, options: options, session: session?.asyncSession).wait() } /** diff --git a/Tests/MongoSwiftSyncTests/SyncMongoClientTests.swift b/Tests/MongoSwiftSyncTests/SyncMongoClientTests.swift index 4788af888..f9703721d 100644 --- a/Tests/MongoSwiftSyncTests/SyncMongoClientTests.swift +++ b/Tests/MongoSwiftSyncTests/SyncMongoClientTests.swift @@ -48,6 +48,29 @@ final class SyncMongoClientTests: MongoSwiftTestCase { if MongoSwiftTestCase.topologyType == .sharded { expect(dbInfo.first?.shards).toNot(beNil()) } + + let monitor = client.addCommandMonitor() + + try monitor.captureEvents { + var opts = ListDatabasesOptions(authorizedDatabases: true) + _ = try client.listDatabaseNames(nil, options: opts, session: nil) + opts.authorizedDatabases = false + _ = try client.listDatabaseNames(nil, options: opts, session: nil) + _ = try client.listDatabaseNames() + } + + let events = monitor.commandStartedEvents() + expect(events).to(haveCount(3)) + + let listDbsAuthTrue = events[0] + expect(listDbsAuthTrue.command["listDatabases"]).toNot(beNil()) + expect(listDbsAuthTrue.command["authorizedDatabases"]?.boolValue).to(beTrue()) + let listDbsAuthFalse = events[1] + expect(listDbsAuthFalse.command["listDatabases"]).toNot(beNil()) + expect(listDbsAuthFalse.command["authorizedDatabases"]?.boolValue).to(beFalse()) + let listDbsAuthNil = events[2] + expect(listDbsAuthNil.command["listDatabases"]).toNot(beNil()) + expect(listDbsAuthNil.command["authorizedDatabases"]).to(beNil()) } func testFailedClientInitialization() {