Skip to content

Commit

Permalink
Merge #260
Browse files Browse the repository at this point in the history
260: Changes related to the next Meilisearch release (v0.26.0) r=bidoubiwa a=meili-bot

Related to this issue: meilisearch/integration-guides#181

This PR:
- gathers the changes related to the next Meilisearch release (v0.26.0) so that this package is ready when the official release is out.
- should pass the tests against the [latest pre-release of Meilisearch](https://github.com/meilisearch/meilisearch/releases).
- might eventually contain test failures until the Meilisearch v0.26.0 is out.

⚠️ This PR should NOT be merged until the next release of Meilisearch (v0.26.0) is out.

_This PR is auto-generated for the [pre-release week](https://github.com/meilisearch/integration-guides/blob/master/guides/pre-release-week.md) purpose._


Co-authored-by: meili-bot <74670311+meili-bot@users.noreply.github.com>
Co-authored-by: cvermand <33010418+bidoubiwa@users.noreply.github.com>
Co-authored-by: Bruno Casali <brunoocasali@gmail.com>
Co-authored-by: meili-bors[bot] <89034592+meili-bors[bot]@users.noreply.github.com>
  • Loading branch information
4 people committed Apr 12, 2022
2 parents cf513b2 + 794c9c9 commit 86d0c58
Show file tree
Hide file tree
Showing 20 changed files with 785 additions and 143 deletions.
29 changes: 29 additions & 0 deletions .code-samples.meilisearch.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -1261,3 +1261,32 @@ geosearch_guide_sort_usage_2: |-
print(error)
}
}
tenant_token_guide_generate_sdk_1: |-
let apiKey = "B5KdX2MY2jV6EXfUs6scSfmC..."
let expiresAt = Date.distantFuture
let searchRules = SearchRulesGroup(SearchRules("patient_medical_records", filter: "user_id = 1"))
client.generateTenantToken(
searchRules,
apiKey: apiKey, // optional
expiresAt: expiresAt // optional
) { (result: Result<String, Error>) in
switch result {
case .success(let token):
print(token)
case .failure(let error):
print(error)
}
}
tenant_token_guide_search_sdk_1: |-
let frontEndClient = MeiliSearch(host: "http://localhost:7700", apiKey: token)
client.index("patient_medical_records")
.search(parameters) { (result: Result<SearchResult<Record>, Swift.Error>) in
switch result {
case .success(let searchResult):
print(searchResult)
case .failure(let error):
print(error)
}
}
13 changes: 11 additions & 2 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,27 @@ import PackageDescription

let package = Package(
name: "meilisearch-swift",
platforms: [
.iOS(.v12),
.tvOS(.v12),
.watchOS(.v5),
.macOS(.v10_15)
],
products: [
.library(name: "MeiliSearch", targets: ["MeiliSearch"])
],
dependencies: [
// Support for dependabot for swift packages in dependabot
// is in draft mode https://github.com/dependabot/dependabot-core/pull/3772
.package(url: "https://github.com/realm/SwiftLint.git", from: "0.43.1")
.package(url: "https://github.com/realm/SwiftLint.git", from: "0.43.1"),
.package(url: "https://github.com/vapor/jwt-kit.git", from: "4.0.0")
],
targets: [
.target(
name: "MeiliSearch",
dependencies: []
dependencies: [
.product(name: "JWTKit", package: "jwt-kit")
]
),
.testTarget(
name: "MeiliSearchUnitTests",
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -190,7 +190,7 @@ Since Meilisearch is typo-tolerant, the movie `philadelphia` is a valid search r

## 🤖 Compatibility with Meilisearch

This package only guarantees the compatibility with the [version v0.25.0 of Meilisearch](https://github.com/meilisearch/meilisearch/releases/tag/v0.25.0).
This package only guarantees the compatibility with the [version v0.26.0 of Meilisearch](https://github.com/meilisearch/meilisearch/releases/tag/v0.26.0).

## 💡 Learn More

Expand Down
24 changes: 24 additions & 0 deletions Sources/MeiliSearch/Client.swift
Original file line number Diff line number Diff line change
Expand Up @@ -270,6 +270,30 @@ public struct MeiliSearch {
)
}

/**
Generates a new tenant token.
- parameter searchRules: A `SearchRulesGroup` provides the rules enforced at search time.
- parameter apiKey: The API key that creates the token. If you leave it empty the client API Key will be used.
- parameter expiresAt: The `Date` at which the token will expire.
- parameter completion: The completion closure will returns a `Result` object that contains token `String` value.
If the token was created successfully or `Error` if a failure occured.
- [docs.meilisearch.com](https://docs.meilisearch.com/learn/security/tenant_tokens.html)
*/
public func generateTenantToken(
_ searchRules: SearchRulesGroup,
apiKey: String? = nil,
expiresAt: Date? = nil,
_ completion: @escaping (Result<String, Swift.Error>) -> Void
) {
TenantTokens.generateTenantToken(
searchRules,
apiKey: apiKey ?? self.config.apiKey ?? "",
expiresAt: expiresAt,
completion
)
}

// MARK: Stats

/**
Expand Down
61 changes: 61 additions & 0 deletions Sources/MeiliSearch/Model/SearchRules.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import Foundation

public struct SearchRules: Codable, Equatable {
var index: String = "*"
var filter: String?

init(_ index: String, filter: String? = nil) {
self.index = index
self.filter = filter
}

public init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()

if let indexes = try? container.decode([String].self), !indexes.isEmpty, let index = indexes.first {
self.index = index

return
}

if let dict = try? container.decode([String: [String: String]].self) {
if let key = dict.keys.first {
self.index = key

if let value = dict[key] {
self.filter = value["filter"]
}
}

return
}

throw DecodingError.typeMismatch(
SearchRules.self,
DecodingError.Context(codingPath: decoder.codingPath, debugDescription: "Wrong type for SearchRules")
)
}

public func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
var dict = [String: [String: String?]]()

guard let filter = self.filter, !filter.isEmpty else {
try container.encode([index])

return
}

dict[index] = ["filter": self.filter]

try container.encode(dict)
}

public func getFilterDict() -> [String: String?]? {
guard let filter = self.filter, !filter.isEmpty else {
return [String: String?]()
}

return ["filter": self.filter]
}
}
53 changes: 53 additions & 0 deletions Sources/MeiliSearch/Model/SearchRulesGroup.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import Foundation

public struct SearchRulesGroup: Codable, Equatable {
var members: [SearchRules] = []

init(_ members: [SearchRules]) {
precondition(members.count > 0, "One or more SearchRules() are required")

self.members = members
}

init(_ searchRules: SearchRules) {
self.init([searchRules])
}

public init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()

if let list = try? container.decode([String].self) {
for item in list {
self.members.append(SearchRules(item))
}

return
}

if let list = try? container.decode([String: [String: String]].self) {
for (index, value) in list {
self.members.append(SearchRules(index, filter: value["filter"] ?? nil))
}

return
}

throw DecodingError.typeMismatch(
SearchRules.self,
DecodingError.Context(codingPath: decoder.codingPath, debugDescription: "Wrong type for SearchRules")
)
}

public func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
var dict = [String: [String: String?]]()

for rule in self.members {
if let filter = rule.getFilterDict() {
dict[rule.index] = filter
}
}

try container.encode(dict)
}
}
21 changes: 21 additions & 0 deletions Sources/MeiliSearch/Model/TokenPayload.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import Foundation
import JWTKit

internal struct TokenPayload: JWTPayload, Equatable {
/// The "exp" (expiration time) claim identifies the expiration time on
/// or after which the JWT MUST NOT be accepted for processing.
var exp: ExpirationClaim?

/// The "searchRules" claim contains the rules to be enforced at
/// search time for all or specific accessible indexes for the signing API Key.
var searchRules: SearchRulesClaim

/// The "apiKeyPrefix" claim contains the first 8 characters of the
/// Meilisearch API key that generates and signs the Tenant Token.
var apiKeyPrefix: ApiKeyPrefixClaim?

func verify(using signer: JWTSigner) throws {
try self.exp?.verifyNotExpired()
try self.apiKeyPrefix?.verify()
}
}
19 changes: 19 additions & 0 deletions Sources/MeiliSearch/TenantTokenClaims/ApiKeyPrefixClaim.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import Foundation
import JWTKit

internal struct ApiKeyPrefixClaim: JWTClaim, Equatable {
/// See `JWTClaim`.
public var value: String

/// See `JWTClaim`.
public init(value: String) {
self.value = value
}

// Checks if the apiKey sent as value is valid to sign the JWT.
public func verify() throws {
if self.value.isEmpty || self.value.count < 8 {
throw JWTError.claimVerificationFailure(name: "apiKeyPrefix", reason: "invalid key sent")
}
}
}
10 changes: 10 additions & 0 deletions Sources/MeiliSearch/TenantTokenClaims/SearchRulesClaim.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import Foundation
import JWTKit

internal struct SearchRulesClaim: JWTClaim, Equatable {
public var value: SearchRulesGroup

public init(value: SearchRulesGroup) {
self.value = value
}
}
55 changes: 55 additions & 0 deletions Sources/MeiliSearch/TenantTokens.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import Foundation
import JWTKit

/// Struct used to generate tenant tokens
internal struct TenantTokens {
/**
Generate tenant tokens with a particular set of options, and use this token to authorize search requests.
- [docs.meilisearch.com](https://docs.meilisearch.com/learn/security/tenant_tokens.html)
*/
static func generateTenantToken(
_ searchRules: SearchRulesGroup,
apiKey: String = "",
expiresAt: Date? = nil,
_ completion: @escaping (Result<String, Swift.Error>) -> Void
) {
let signers = JWTSigners()
signers.use(.hs256(key: apiKey))

do {
let payload = try createTokenPayload(
searchRules: searchRules,
apiKey: apiKey,
expiresAt: expiresAt
)

let jwt = try signers.sign(payload)

completion(.success(jwt))
} catch let error {
completion(.failure(error))
}
}

private static func createTokenPayload(
searchRules: SearchRulesGroup,
apiKey: String,
expiresAt: Date?
) throws -> TokenPayload {
guard !apiKey.isEmpty else { throw JWTError.signatureVerifictionFailed }

var payload = TokenPayload(
searchRules: SearchRulesClaim(value: searchRules),
apiKeyPrefix: ApiKeyPrefixClaim(value: String(apiKey.prefix(8)))
)

if let exp = expiresAt {
payload.exp = ExpirationClaim(value: exp)
}

try payload.verify(using: .hs256(key: apiKey))

return payload
}
}
12 changes: 0 additions & 12 deletions Tests/MeiliSearchIntegrationTests/DocumentsTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,6 @@ import Foundation

// swiftlint:disable force_unwrapping
// swiftlint:disable force_try
private struct Movie: Codable, Equatable {
let id: Int
let title: String
let comment: String?

init(id: Int, title: String, comment: String? = nil) {
self.id = id
self.title = title
self.comment = comment
}
}

private let movies: [Movie] = [
Movie(id: 123, title: "Pride and Prejudice", comment: "A great book"),
Movie(id: 456, title: "Le Petit Prince", comment: "A french book"),
Expand Down
12 changes: 0 additions & 12 deletions Tests/MeiliSearchIntegrationTests/KeysTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,6 @@ import XCTest
import Foundation

// swiftlint:disable force_try
private struct Movie: Codable, Equatable {
let id: Int
let title: String
let comment: String?

init(id: Int, title: String, comment: String? = nil) {
self.id = id
self.title = title
self.comment = comment
}
}

class KeysTests: XCTestCase {
private var client: MeiliSearch!
private var key: String = ""
Expand Down
Loading

0 comments on commit 86d0c58

Please sign in to comment.