4 ms·
1. use UUIDs for user IDs 2. use typed schemas for APIs, i.e. GraphQL with a custom scalar type for UserID. Other typed schemas for APIs: OpenAPI, AsyncAPI, p
by nivertech 4y ago
1. use UUIDs for user IDs
2. use typed schemas for APIs, i.e. GraphQL with a custom scalar type for UserID. Other typed schemas for APIs: OpenAPI, AsyncAPI, protobufs/gRPC, etc.
// add GraphQL parsing and serialization code
scalar UserID
3. Make the implicit - explicit. Don't use naked primitive types in your code (similarly as you will not use naked literal constants, i.e. use PI instead of 3.1415...). Use a PL with the string static typing (preferably Algebraic Type System). Define type for UserID which cannot be mixed with the integers, i.e.
type UserID = UserID int
4. in dynamic PLs you can use tagged tuples or tagged maps/structs, i.e.
{:user_id, 1234}
or
{"_type": "UserID", "id": 1234}
5. validate all external inputs (even those comming from the DB or Message Broker):
validateUserID: int -> UserID
6. The examples above assume integer representation of the user id, here's if we switch to UUIDv4:
type UUIDv4 = UUIDv4 string
type UserID = UserID UUIDv4
validateUserID: string -> UserID
- nivertech 4y agos/comming from/coming from/ s/string static typing/strict static typing/ as opposed to the weak static typing in languages like C/C++/etc.
- colonwqbang 4y agoThe type system of C is not as good as some later ones, but it's perfectly sufficient to prevent errors of the type shown in TFA. struct user_id { uint64_t val; }; void ban_user(struct user_id id);
- nivertech 4y agoyes, here's the correct code: #include <stdio.h> #include <stdint.h> typedef struct user_id { uint64_t val; } user_id_t; void ban_user(user_id_t id) { printf("user with id %lu was banned", id.val); } int main() { user_id_t someUser = {123}; ban_user(someUser); return 0; } output: > clang-7 -pthread -lm -o main main.c > ./main user with id 123 was banned And here's the code with the bug: #include <stdio.h> #include <stdint.h> typedef struct user_id { uint64_t val; } user_id_t; typedef struct group_id { uint64_t val; } group_id_t; void ban_user(user_id_t id) { printf("user with id %lu was banned", id.val); } int main() { user_id_t someUser = {123}; ban_user(someUser); group_id_t someGroup = {456}; ban_user(someGroup); return 0; } Compiler output: > clang-7 -pthread -lm -o main main.c main.c:22:12: error: passing 'group_id_t' (aka 'struct group_id') to parameter of incompatible type 'user_id_t' (aka 'struct user_id') ban_user(someGroup); ^~~~~~~~~ main.c:13:25: note: passing argument to parameter 'id' here void ban_user(user_id_t id) { ^ 1 error generated. exit status 1
- LorenPechtel 4y agoAnd if you use UUIDs for user IDs you're probably using them for other things. The same sort of mistake can happen. The only real defense here is language-level enforcement. Allow declaration of a subtype that is not assignment-compatible with the parent even though it's identical. I don't do any web-facing stuff but the only bits of code that know about things like IDs are the database stuff. All the logic works with classes that contain the ID and relevant data--you always pass the class, not the ID.
- 10000truths 4y agoIf you’re using randomly generated UUIDs, then collisions of any kind should be extremely unlikely, no matter how you’re using them. <0.2% chance in 2^60 random UUIDs.
- unscaled 4y agoUUID also suffers from a serious issue: there is no standard way to encode them as string and in binary format. In binary, UUID can be encoded as Big Endian or Little Endian or even Mixed Endian, and while the string encoding is usually lowercase with a certain hyphenization rules, I've seen variations of that. The (hexadecimal) text encoding is also quite inefficient compared to more modern standards like ULID or kSUID.
- masklinn 4y ago> In binary, UUID can be encoded as Big Endian or Little Endian or even Mixed Endian RFC 4122 section 4.1.2: > The fields are presented with the most significant one first. > while the string encoding is usually lowercase with a certain hyphenization rules, I've seen variations of that. RFC 4122 section 3: > The hexadecimal values "a" through "f" are output as lower case characters and are case insensitive on input. The hyphenation is normative, per the same section UUID = time-low "-" time-mid "-" time-high-and-version "-" clock-seq-and-reserved clock-seq-low "-" node
- thangngoc89 4y agoAre you familiar with Typescript? If it's possible, could you give me an example of 3 or 4 in Typescript?
- nivertech 4y agoI don't use TypeScript and I didn't found how to do proper ML-style wrapper types in TypeScript. But I guess one can use type aliases for that: type UserID = { userID: number; }; let validateUserID = function (x: number): UserID? { ... }; Note, that my original example was missing the fact that not every input value is a valid user ID, i.e. the function should return Maybe<UserID>, Option<UserID>, or Result<UserID,SomeErrorType>. In TypeScript one can use optional types xxx? instead.
- thangngoc89 4y agoI was deep into ReasonML/OCaml in the last coupe of years so yeah I missed option/result and the ability to define new type on top of the primitive ones. Your example: type UserID = { id: number; } Would result in more allocations. But I think it's the only way at the moment.
- culi 4y agoyour validation function would probably look more like ```ts const validateUserId = (x: number): x is UserId => {}; ``` for the `UserId` type declaration, I'm actually a little unsure of the best way to go about it. But I feel like using enums, typed string literals, or classes might be better approach. Would be interested if anyone has any thoughts on this
- nikeee 4y agoYou can sue Tagged/Branded types for that: https://medium.com/@KevinBGreene/surviving-the-typescript-ecosystem-branding-and-type-tagging-6cf6e516523d https://medium.com/@KevinBGreene/surviving-the-typescript-ec... It's a simple hack that takes a type and adds an additional field to it. You can then use a User-Defined Type-Guard to ensure a value is valid: https://levelup.gitconnected.com/user-defined-type-guards-in-typescript-fad639e4944f https://levelup.gitconnected.com/user-defined-type-guards-in...
- Hermitian909 4y agoMy one critique of this prescription is that UUIDs are not identifiable as user IDs. IMO you're better off with a format like "user{20 random characters}"
- camgunz 4y agoI'll get back on my "tests are better than types" hobby horse and say "or write a single test for this function". The problem here is that an engineer was able to push untested code to prod that irrevocably modifies the database. You need a test here because types won't tell you that a user was banned. And that test would also have caught this error.
- nivertech 4y agoIMO the developer need multiple safety nets from the wide net to the narrow one: - informal methods: - unstructured time to think about the problem (Hammock Driven Development) - write things down = writing is thinking - Rubber Duck Driven Development - design reviews - lightweight formal methods: - Decision Tables - FSMs - FP / Algebraic Type System with rich scalar datatypes - DbC - Design-by-Contract - PBT - Property-based Testing - TDD - example-based tests
- camgunz 4y agoNice, yeah totally. And I would add you shouldn't put all your risk eggs in one safety basket. If you only have types or tests not only are you not covering all your bases, you're asking way too much from a given technique.