Protected is a Swift Package that allows you to specify the read and write rights for any type, depending on context by using Phantom types

Overview

Proteted

Swift Package Manager Twitter: @nerdsupremacist

Protected

Access control can't always be static. Sometimes the mutability, nullability and access of variables depends on context. When dealing with these scenarios, we usually end up writing wrappers or duplicate the class for each different context. Well no more!

Protected is a Swift Package that allows you to specify the read and write rights for any type, depending on context by using Phantom types. Here's a taste of the syntax (we will explain everything in time):

struct MyRights: RightsManifest {
    typealias ProtectedType = Book
    
    let title = Write(\.title)
    let author = Read(\.author)
}

func work(book: Protected) {
    book.title // ✅ works
    book.title = "Don Quixote" // ✅ works
    book.author // ✅ works
    book.author = "" // ❌ will not compile
    book.isbn // ❌ will not compile
}

This project is heavily inspired by @sellmair's post on Phantom Read Rights. For those curious Protected relies on phantom types and dynamic member look up to provide an easy API for specifying read and write rights for any type in Swift.

Installation

Swift Package Manager

You can install Sync via Swift Package Manager by adding the following line to your Package.swift:

import PackageDescription

let package = Package(
    [...]
    dependencies: [
        .package(url: "https://github.com/nerdsupremacist/Protected.git", from: "1.0.0")
    ]
)

Usage

So let's imagine that you run a Book publishing company. Your codebase works with information about books at different stages of publishing. Most of the code revolves entirely around the following class:

public class Book {
    public var title: String?
    public var author: String?
    public var isbn: String?
}

So what's wrong with this code? Well plenty of things:

  1. Everything is nullable. Despite the fact that there's places in our code where we can be sure that they're not null anymore.
  2. Everyting can be read publicly.
  3. Everything is mutable, all of the time. And if anything is mutable, you can bet someone will mutate it, and probably in a part of code where you are not expecting it.

One way to address this would be to create a different version of Book for every scenario: PlannedBook, PrePublishingBook, PostPublishingBook, PublishedBook, etc. But this leads to an unsustainable amount of code duplication and added complexity. These things might not look to bad when it comes to a simple class with three attributes, but as your classes get more complicated and we get more and more cases, keeping track of what can be read and mutated where becomes very difficult.

Enter our package Protected. When working with Protected, you write your model once, and we change how you access it. We are mainly working with two things:

  1. RightsManifests: basically a type that specifies to what you have access to and how much.
  2. Protected: a wrapper that will enforce at compile time that you only read and write what's allowed by the manifest.

So for our book example, we can consider that we want to safely handle the pre-publishing stage of a book. At this stage the author name is already set and should be changed. The title is also set, but is open to change. The ISBN should not be read at all. For this case we can write a RightsManifest

struct PrePublishRights: RightsManifest {
    typealias ProtectedType = Book

    // a) Declare that we can read and write the title
    let title = Write(\.title!) // b) with the ! enforce that at this stage it's no longer optional
    // c) Declare that we can only read the name of the author
    let author = Read(\.author!)
    
    // Do not include any declaration for the ISBN
}

A RightsManifest is a type that includes variables pointing to either:

  • Write: can be read be written to
  • Read: can only be read

Each attribute you declare in the manifest can then be read in that context. So let's try to use it:

let book = Protected(Book(), by: PrePublishRights())
book.title // ✅ works
book.title = "Don Quixote" // ✅ works
book.author // ✅ works
book.author = "" // ❌ will not compile
book.isbn // ❌ will not compile

More Advanced Features

Protecting nested types

If your object contains nested types, you can specify in your manifest, the manifest that corresponds to that value, and Protected will in that case return a Protected Value For example, let's say that your books point to an Author object where you quite insecurely store the password (I've seen worse security):

class Author {
    var name: String?
    var password: String?
}

class Book {
    var title: String?
    var author: Author?
}

And let's say that you want to make sure that when someone grabs the author object from your book, that they can't see the password either. For that you can start by creating the manifests for both types. And when it comes to specifying the read right to the author, you can include that it should be protected by your other Manifest:

struct AuthorBasicRights: RightsManifest {
    typealias ProtectedType = Author
    
    let name = Read(\.name)
}

struct BookBasicRights: RightsManifest {
    typealias ProtectedType = Book
    
    let title = Write(\.title)
    // specify that for the author you want the result to be protected by AuthorBasicRights
    let author = Read(\.author).protected(by: AuthorBasicRights())
}

With this when you try to use it, you won't be able to access the password:

let book = Protected(Book(), by: BookBasicRights())
book.title // ✅ works
let author = book.author // returns a Protected?
author?.name // ✅ works
author?.password // ❌ will not compile

Manipulating Values and Changing Rights

All Protected values are designed to be changed. If you use the same object at different stages, you would like to change the rights associated with that object at any given time. That's why Protected comes with a couple of functions prefixed by unsafeX to signal that you really should know what it is that you're doing with the object here.

For example let's imagine that you're writing a piece of code that will create an ISBN for a book and move it to the post publishing stage. So you can imagine that your rights look as follows:

struct PrePublishRights: RightsManifest {
    typealias ProtectedType = Book

    let title = Write(\.title!)
    let author = Read(\.author!)
}

struct PostPublishRights: RightsManifest {
    typealias ProtectedType = Book

    let title = Read(\.title!)
    let author = Read(\.author!)
    let isbn = Read(\.isbn!)
}

When you publish the book, you will efectively transition your object to be governed by the pre publish rights to the post publish rights. You can do this with the method: unsafeMutateAndChangeRights:

func publish(book: Protected) -> Protected {
    return book.unsafeMutateAndChangeRights(to: PostPublishRights()) { book in 
        // here you have complete unsafe access to the underlying `book` object, absolutely no limitations
        book.isbn = generateISBN()
    }
}

Other unsafeX functions to deal with the underlying data when needed include:

  • unsafeMutate: let's you mutate the underlying value however you like.
  • unsafeChangeRights: let's you create a new version of the protected, governed by a new manifest.
  • unsafeMapAndChangeRights: let's you map the value onto a new one, and wrap it in a new protected governed by a different manifest.
  • unsafeBypassRights: just get the value no matter what the manifest says.

More elaborate Read rights

Read rights don't necessarily need to be a keypath. For Read Rights you have multiple options for dealing with them. For example you can provide a more elaborate getter logic:

struct AuthorBasicRights: RightsManifest {
    typealias ProtectedType = Author
    
    let name = Read(\.name)
    let password = Read { obfuscate($0.password) }
}

You can also include a .map after any Read to manipulate the value:

struct AuthorBasicRights: RightsManifest {
    typealias ProtectedType = Author
    
    let name = Read(\.name)
    let password = Read(\.password).map { obfuscate($0) }
}

Caveats

This is not a perfect protection for no one to be able to access things they shouldn't. Protected is not a security framework, it will not prevent people from accessing or mutating anything. It is intended as an easy way to make safe usage clear and simple depending on context.

  1. A code can always access everything using the unsafeX methods provided.
  2. You can (but really shouldn't) include more rights whithin the extension of a manifest. This allows you to include more rights than intended while still appearing to be safe. Do not do this! Protected cannot protect you from doing this.

Contributions

Contributions are welcome and encouraged!

License

Protected is available under the MIT license. See the LICENSE file for more info.

You might also like...
ZIP Foundation is a library to create, read and modify ZIP archive files.
ZIP Foundation is a library to create, read and modify ZIP archive files.

ZIP Foundation is a library to create, read and modify ZIP archive files. It is written in Swift and based on Apple's libcompression for high performa

A NEWS app which can be used to read,share and bookmark articles of various categories
A NEWS app which can be used to read,share and bookmark articles of various categories

Scoop A NEWS App for iOS 14 built using Swift which allow the users to read,bookmark and share news articles. Built using MVC architecture Requirement

A lightweight extension to Swift's CollectionDifference, supporting moves in addition to removals and insertions, critical when updating interfaces and managing reference types.

DifferenceTracker is a lightweight extension to Swift's CollectionDifference. It defines moves in addition to removals and insertions, critical when updating interfaces and managing reference types.

Write Emacs packages in Swift!

EmacsSwiftModule A Swift library to write Emacs plugins in Swift! Overview Emacs Swift module provides a convenient API for writing dynamic modules fo

Project shows how to unit test asynchronous API calls in Swift using Mocking without using any 3rd party software

UnitTestingNetworkCalls-Swift Project shows how to unit test asynchronous API ca

Read iOS 15 privacy insight '.ndjson' file into your human brain.
Read iOS 15 privacy insight '.ndjson' file into your human brain.

Insight Read iOS 15 privacy insight '.ndjson' file into your human brain. Written in SwiftUI. Feature Compile records into app summary Relink app info

How Swift standard types and classes were supposed to work.
How Swift standard types and classes were supposed to work.

How Swift standard types and classes were supposed to work. A collection of useful extensions for the Swift Standard Library, Foundation, and UIKit.

Extensions for Swift Standard Types and Classes

Cent Cent is a library that extends certain Swift object types using the extension feature and gives its two cents to Swift language. Dollar is a Swif

A reverse engineering tool to restore stripped symbol table and dump Objective-C class or Swift types for machO file.

A reverse engineering tool to restore stripped symbol table and dump Objective-C class or Swift types for machO file.

Releases(1.0.0)
Owner
Mathias Quintero
Developer, Student, Spaghetti Code Enthusiast and Professional Swear Word Sayer.
Mathias Quintero
MediaType is a library that can be used to create Media Types in a type-safe manner.

This is a general purpose Swift library for a concept of typed treatment for Media Types. We use this library on clients and servers to speak the same dialect and to enjoy all the comfort strong types provide over raw strings.

21Gram Consulting 79 Jul 19, 2022
Swift package adding fraction and percentage types.

swift-rationals Rationals is a package containing Fraction and Percentage types for the Swift programming language. Contents The package currently pro

Alexandre H. Saad 0 Jul 28, 2022
Numerals is a package containing additional numeric types for the Swift programming language.

swift-numerals Numerals is a package containing additional numeric types for the Swift programming language. Contents The package currently provides t

Alexandre H. Saad 0 Jul 28, 2022
Swift package adding measurable types.

swift-measures Measures is a package containing measurable types for the Swift programming language. Contents The package currently provides the follo

Alexandre H. Saad 1 Nov 5, 2022
Parsing indeterminate types with Decodable and Either enum using Swift

Decodable + Either Parsing indeterminate types with Decodable and Either enum us

Alonso Alvarez 1 Jan 9, 2022
Swift package for accessing SF Symbols in a type safe manner.

Swift Package Information Code Coverage Swift package for accessing SF Symbols in a type safe manner. Features ?? Contains all SF Symbols - 1.0, 2.0,

null 6 Dec 7, 2021
Swift-DocC is a documentation compiler for Swift frameworks and packages aimed at making it easy to write and publish great developer documentation.

Swift-DocC is a documentation compiler for Swift frameworks and packages aimed at making it easy to write and publish great developer docum

Apple 833 Jan 3, 2023
Swift Package Manager plugin which runs ActionBuilder to create a Github Actions workflow for a swift package.

ActionBuilderPlugin A Swift Package Manager command which builds a Github Actions workflow for the current package. By default the workflow file will

Elegant Chaos 4 Jul 20, 2022
This is a Swift package with support for macOS that allows to start Java Jar's with the default or a custom JVM.

Jar.swift jar runner for macos Jar.swift is created and maintaned with ❥ by Sascha Muellner. What? This is a Swift package with support for macOS that

Swift Package Repository 1 Nov 11, 2021
Repository for all the programs and Code I would write in Swift.

Swift Programming Language This repository contains the program and codes I write in Swift About Swift Swift is a general-purpose, multi-paradigm, com

Kannan Jayachandran 2 Sep 21, 2022