Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
117 changes: 117 additions & 0 deletions Sources/Biscuits/Authorization.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
/*
* Copyright (c) 2025 Contributors to the Eclipse Foundation.
* SPDX-License-Identifier: Apache-2.0
*/

extension Biscuit {
/// The result of a successful authorization check on a Biscuit
public struct Authorization: Sendable, Hashable {
/// Which policy statement passed, resulting in the successful authorization
public let successfulPolicy: Policy

let resolution: Resolution

init(policy: Policy, resolution: Resolution) {
self.successfulPolicy = policy
self.resolution = resolution
}

/// Query the authorization to check if a certain statement holds true
/// - Parameter check: The Check to use to query the authorization
/// - Returns: Whether or not the query succeeded
/// - Throws: Throws an `EvaluationError` if the query cannot be evaluated
public func query(using check: Check) throws -> Bool {
try check.validate(self.resolution, [0], .authorizer)
}

/// Query the authorization to check if a certain statement holds true
/// - Parameter kind: What kind of check to perform
/// - Parameter trusting: identities to trust when evaluating this query
/// - Parameter predicates: The predicates of this query
/// - Returns: Whether or not the query succeeded
/// - Throws: Throws an `EvaluationError` if the query cannot be evaluated
public func query<each T: TrustedScopeConvertible>(
kind: Check.Kind = .checkIf,
trusting: repeat each T,
@Biscuit.StatementBuilder predicates: () -> Biscuit.StatementBuilder
) throws -> Bool {
var scopes: [TrustedScope] = []
repeat scopes.append((each trusting).trustedScope)
return try self.query(using: Check(kind: kind, trusting: scopes, predicates()))
}

/// Query the authorization to check if a certain statement holds true
/// - Parameter datalog: The check to use to query the authorization, as a String
/// - Returns: Whether or not the query succeeded
/// - Throws: Throws an `EvaluationError` if the query cannot be evaluated and parsing the
/// Datalog may throw a `DatalogError`
public func query(using datalog: String) throws -> Bool {
try self.query(using: Check(datalog))
}

/// Query the authorization to extract values from it
///
/// Some number of names and types is passed as the "result" parameter; these should be the
/// names of variables that appear in the predicates of the query. This query will return
/// tuples of values that satisfy the predicates when bound to those names.
///
/// - Parameter result: a sequence of tuples containing the name of a variable used in the
/// query and the type that variable is expected to be
/// - Parameter trusting: identities to trust when evaluating this query
/// - Parameter predicates: The predicates of this query
/// - Returns: An array of all the tuples of values which satisfy the predicates
/// - Throws: Throws an `EvaluationError` if the query cannot be evaluated, an
/// `InvalidQueryError` if the query is invalid or an `InvalidValueError` if a value does
/// not have the expected type
public func queryValues<each V: ExpressibleByValue, each T: TrustedScopeConvertible>(
result: repeat (String, (each V).Type),
trusting: repeat each T,
@Biscuit.StatementBuilder predicates: () throws -> Biscuit.StatementBuilder
) throws -> [(repeat each V)] {
let query = try predicates()
var scopes: [TrustedScope] = []
repeat scopes.append((each trusting).trustedScope)
let trusted = self.resolution.trustScopes(scopes, nil)
let variables = try self.resolution.queryValues(query.predicates, query.expressions, trusted)
return try variables.map { vars in
(repeat try unpackVariable(vars, (each result).0, (each result).1))
}
}

/// Query the authorization to extract values from it, expecting only one tuple of matching
/// values.
///
/// Some number of names and types is passed as the "result" parameter; these should be the
/// names of variables that appear in the predicates of the query. This query will return
/// tuples of values that satisfy the predicates when bound to those names.
///
/// - Parameter result: a sequence of tuples containing the name of a variable used in the
/// query and the type that variable is expected to be
/// - Parameter trusting: identities to trust when evaluating this query
/// - Parameter predicates: The predicates of this query
/// - Returns: The tuples of values which satisfy the predicates
/// - Throws: Throws an `EvaluationError` if the query cannot be evaluated, an
/// `InvalidQueryError` if the query is invalid or returns too many or too few results, or
/// an `InvalidValueError` if a value does not have the expected type
public func queryValuesExpectingOne<each V: ExpressibleByValue, each T: TrustedScopeConvertible>(
result: repeat (String, (each V).Type),
trusting: repeat each T,
@Biscuit.StatementBuilder predicates: () throws -> Biscuit.StatementBuilder
) throws -> (repeat each V) {
let query = try predicates()
var scopes: [TrustedScope] = []
repeat scopes.append((each trusting).trustedScope)
let trusted = self.resolution.trustScopes(scopes, nil)
let variables = try self.resolution.queryValuesExpectingOne(query.predicates, query.expressions, trusted)
return (repeat try unpackVariable(variables, (each result).0, (each result).1))
}
}
}

func unpackVariable<V: ExpressibleByValue>(_ vars: [String: Value], _ name: String, _ ty: V.Type) throws -> V {
if let value = vars[name] {
return try ty.init(value: value)
} else {
throw Biscuit.InvalidQueryError.missingVariable
}
}
70 changes: 60 additions & 10 deletions Sources/Biscuits/Biscuit.swift
Original file line number Diff line number Diff line change
Expand Up @@ -443,16 +443,6 @@ public struct Biscuit: Sendable, Hashable {
public var debugDescription: String { String(reflecting: self.rawValue) }
}

/// The result of a successful authorization check on a Biscuit
public struct Authorization: Sendable, Hashable {
/// Which policy statement passed, resulting in the successful authorization
public let successfulPolicy: Policy

init(policy: Policy) {
self.successfulPolicy = policy
}
}

/// Query the biscuit to check if a certain statement holds true
/// - Parameter check: The Check to use to query the biscuit
/// - Parameter limitedBy: Limitations on the runtime for this query
Expand Down Expand Up @@ -503,4 +493,64 @@ public struct Biscuit: Sendable, Hashable {
var lastBlock: Block {
self.attenuations.last ?? self.authority
}
/// Query the biscuit to extract values from it
///
/// Some number of names and types is passed as the "result" parameter; these should be the
/// names of variables that appear in the predicates of the query. This query will return
/// tuples of values that satisfy the predicates when bound to those names.
///
/// - Parameter result: a sequence of tuples containing the name of a variable used in the
/// query and the type that variable is expected to be
/// - Parameter trusting: identities to trust when evaluating this query
/// - Parameter predicates: The predicates of this query
/// - Returns: An array of all the tuples of values which satisfy the predicates
/// - Throws: Throws an `AuthorizationError` if the biscuit does not pass authorization, or an
/// `EvaluationError` if the query cannot be evaluated, an `InvalidQueryError` if the query is
/// invalid or an `InvalidValueError` if a value does not have the expected type
public func queryValues<each V: ExpressibleByValue, each T: TrustedScopeConvertible>(
result: repeat (String, (each V).Type),
trusting: repeat each T,
limitedBy: Authorizer.Limits = Authorizer.Limits.noLimits,
@Biscuit.StatementBuilder predicates: () throws -> Biscuit.StatementBuilder
) throws -> [(repeat each V)] {
let resolution = try Resolution(biscuit: self, authorizer: Authorizer(limits: limitedBy))
let query = try predicates()
var scopes: [TrustedScope] = []
repeat scopes.append((each trusting).trustedScope)
let trusted = resolution.trustScopes(scopes, nil)
let variables = try resolution.queryValues(query.predicates, query.expressions, trusted)
return try variables.map { vars in
(repeat try unpackVariable(vars, (each result).0, (each result).1))
}
}

/// Query the biscuit to extract values from it, expecting only one tuple of matching values.
///
/// Some number of names and types is passed as the "result" parameter; these should be the
/// names of variables that appear in the predicates of the query. This query will return tuples
/// of values that satisfy the predicates when bound to those names.
///
/// - Parameter result: a sequence of tuples containing the name of a variable used in the
/// query and the type that variable is expected to be
/// - Parameter trusting: identities to trust when evaluating this query
/// - Parameter predicates: The predicates of this query
/// - Returns: The tuples of values which satisfy the predicates
/// - Throws: Throws an `AuthorizationError` if the biscuit does not pass authorization, or an
/// `EvaluationError` if the query cannot be evaluated, an `InvalidQueryError` if the query is
/// invalid or returns too many or too few results, or an `InvalidValueError` if a value does
/// not have the expected type
public func queryValuesExpectingOne<each V: ExpressibleByValue, each T: TrustedScopeConvertible>(
result: repeat (String, (each V).Type),
trusting: repeat each T,
limitedBy: Authorizer.Limits = Authorizer.Limits.noLimits,
@Biscuit.StatementBuilder predicates: () throws -> Biscuit.StatementBuilder
) throws -> (repeat each V) {
let resolution = try Resolution(biscuit: self, authorizer: Authorizer(limits: limitedBy))
let query = try predicates()
var scopes: [TrustedScope] = []
repeat scopes.append((each trusting).trustedScope)
let trusted = resolution.trustScopes(scopes, nil)
let variables = try resolution.queryValuesExpectingOne(query.predicates, query.expressions, trusted)
return (repeat try unpackVariable(variables, (each result).0, (each result).1))
}
}
3 changes: 2 additions & 1 deletion Sources/Biscuits/Datalog/Authorizer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
* Copyright (c) 2025 Contributors to the Eclipse Foundation.
* SPDX-License-Identifier: Apache-2.0
*/

extension Biscuit {
func validateChecks(_ resolution: Resolution) throws {
try self.authority.validateChecks(resolution, .block(0))
Expand Down Expand Up @@ -202,7 +203,7 @@ extension Policy {
if try resolution.checkQueryIf(query, trusted) {
switch self.kind.wrapped {
case .allow:
return Biscuit.Authorization(policy: self)
return Biscuit.Authorization(policy: self, resolution: resolution)
case .deny:
throw Biscuit.AuthorizationError(deny: self)
}
Expand Down
30 changes: 27 additions & 3 deletions Sources/Biscuits/Datalog/MapKey.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ import Foundation
#endif

/// A Value that can be used as a key in a map
public struct MapKey: MapKeyConvertible, ValueConvertible, TermConvertible, ExpressionConvertible, Hashable, Sendable,
public struct MapKey: ExpressibleByMapKey, MapKeyConvertible, ValueConvertible, TermConvertible, ExpressionConvertible,
Hashable, Sendable,
CustomStringConvertible
{
internal enum Wrapped: Hashable {
Expand Down Expand Up @@ -37,6 +38,10 @@ public struct MapKey: MapKeyConvertible, ValueConvertible, TermConvertible, Expr
self.wrapped = .string(string)
}

public init(mapKey: MapKey) throws {
self = mapKey
}

init(_ wrapped: Wrapped) {
self.wrapped = wrapped
}
Expand Down Expand Up @@ -72,10 +77,29 @@ public protocol MapKeyConvertible: ValueConvertible {
var mapKey: MapKey { get }
}

extension Int: MapKeyConvertible {
/// Anything which can be expressed as a MapKey
public protocol ExpressibleByMapKey {
init(mapKey: MapKey) throws
}

extension Int: ExpressibleByMapKey, MapKeyConvertible {
public init(mapKey: MapKey) throws {
switch mapKey.wrapped {
case .integer(let int): self = Int(int)
default: throw Biscuit.InvalidValueError(expected: Int.self)
}
}

public var mapKey: MapKey { MapKey(self) }
}

extension String: MapKeyConvertible {
extension String: ExpressibleByMapKey, MapKeyConvertible {
public init(mapKey: MapKey) throws {
switch mapKey.wrapped {
case .string(let string): self = string
default: throw Biscuit.InvalidValueError(expected: String.self)
}
}

public var mapKey: MapKey { MapKey(self) }
}
1 change: 1 addition & 0 deletions Sources/Biscuits/Datalog/Query.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
* Copyright (c) 2025 Contributors to the Eclipse Foundation.
* SPDX-License-Identifier: Apache-2.0
*/

extension Biscuit {
/// A query about a Biscuit that can be true or false.
public struct Query: Sendable, Hashable, CustomStringConvertible {
Expand Down
Loading
Loading