Project Breakdown: Ark (Repost)

I originally posted this on substack, but I decided to switch to this platform.

What is Ark?

notpointless/ark is a full-stack monolithic application that heavily emphasizes its (IAM) solution. Initially, it was supposed to be closed source because it was going to be used for a side project, but I decided to release it to the public for personal reasons.

Technical Breakdown

My thought process and the reasoning behind my decisions during the construction of this project. I will also detail the pros and cons of each aspect of the project, what I could have done better, and alternative approaches.

Task System

The idea behind this is that instead of re-instating the desired database instance to create or update a specific field within a database, we can queue the desired task without calling the database every single time through the function. This ends up streamlining the function.

so instead of this:

pub fn create_permission(database: PostgresDatabase, permission: Permission) -> Result<T> {/* perform operation here */}

we have this:

pub fn create_permission(permission: Permission) -> TaskResult<T> {/*queue message here*/}

So, instead of instantiating it through a CRUD function, the Task System provides the instantiation when it receives the task.

Due to the way the Task System was built, you need to specify what type of task you would like to use when composing a message so it knows what handler to use.

In order to successfully compose and send a task to the Task System to handle; A few things need to be done, you need three things: TaskType, TaskHandler<D>, and Task<D, R, P>.

  • TaskType, will help identify what handler to use.

  • TaskHandler<D> will assign a specific “action” to Task<D, R, P>

    • D: Database

  • Task<D, R, P> will perform the actual task.

    • D: Database

    • R: TaskRequest

    • P: The Type

After all those things are satisified you need to ensure the TaskType is registered with the corresponding listener. You can look at how to register a listener here.

Now you can create a function that sends a request to the task channel.

pub fn create_role(role: Role) -> TaskResult<TaskStatus> {
      let task_request = Self::create_role_request(role);
      TaskManager::process_task(task_request)
}
fn create_role_request(role: Role) -> TaskRequest {
      TaskRequest::compose_request(RoleCreateTask::from(role), TaskType::Role, "role_create")
}

TaskRequest will compose a request that the Task System can interpret then we can call TaskManager::process_task(task_request); this will return a Completed or Failed status. If you’re expecting a custom type to be returned then you can instead call TaskManager::process_task_with_result::<T>(request); instead of process_task.

Channeling

When the Task System recieves a request it first gets sent to the INBOUND channel then it sends its results to the OUTBOUND channel. This creates a bidirectional channels and this is how we can retrieve the results.

Advantages/Disadvantages

The amount of complexity that gets added is astonishing and it’s not a good thing. Also the inability to actually sends tasks within another task creates another issue on its own.

Advantages

  • Function Simplicity

  • Granular Control

Disadvantages

  • Complexity

  • No Nested Tasks

Alternative Approaches (Possible)

  • Use a messaging broker instead of using channels.

  • Use Redis pub/sub instead of channels.

  • Add the tasks to a hashmap first, then have the task system pull directly from the hashmap. The only con is I will need multiple hashmaps for each type. And additional complexity. Not entirely sure if this would solve being unable to call nested tasks.

  • Using serialization to prevent the overuse of generic types.

Cache System

The same as the Task System, but instead of using PostgreSQL, Redis is used. I separated them because I can’t call a task within a task, meaning I cannot call a channel within a channel unless it comes from a different instance. So, instead of calling a task within a task, it is being called from a cache within a task because cache uses a separate instance of channel than the task.

IAM (Identity And Access Management)

The IAM I built for this project is relatively rudimentary. It doesn’t contain effects, resources, or policies. It’s based on the user’s role and permission. So, I guess you could still call it an IAM?

Permission

I integrated permissions entirely because I was looking for something flexible. The permissions are stored inside PostgreSQL but are cached quite differently. You are not supposed to use the Cache System to cache these because they’re not stored inside Redis but locally inside a HashMap. The reason I did was for quantity. You might only expect a couple hundred permissions, but you will expect a couple hundred thousand users.

Caching:

Like I mentioned above the caching mechanism is different. But it is pretty straight forward. If someone wants to use a local cache then can they skip over the Cache System and use LocalizedCache<T> T signifies what type you want to cache. For example: Permission. So you would use impl LocalizedCache<Permission> for PermissionCache then implement the required types.

Whenever you call fn add(item: T), it will add three keys: permission_id, permission_name, and permission_key. I chose to add three other keys because all these fields are unique inside the database; therefore, I want permissions to be retrievable by their ID, name, or key.

Also, the values for all the keys all use an Arc<T> T being the same T for LocalizedCache<T>; therefore, I could implement an fn update() and update a single field, and it will automatically update the other two because they’re a shared state. I just wanted to say this because I’m well aware.

Schema:

  • permission_id: the uuid of the permission (randomly generated using the uuid crate)

  • permission_name: the name of the permission. Example: “Ban User”

  • permission_key: the key of the permission. Example: “admin.ban.user“

The permission_name and permission_key can be in any format. This is just the ideal example of what it should look like.

Relation

The roles and permissions are connected, and to ensure consistency among them, the roles type takes a field called pub role_permissions: Vec<String>. This contains only the id of the permission entirely because that is the only immutable field among a permission.

Role

I think of roles as just a bunch of permissions grouped into one place, so I added that. I wanted predefined permissions for specific roles. So I wouldn’t have to assign, for example, the ban permission for every user; instead, I could assign them the role and adjust the permissions accordingly. It ensures consistently among roles.

Schema:

  • role_id: the uuid of the permission (randomly generated using the uuid crate)

  • role_name: the name of the permission. Example: “Moderator”

The role_name and role_id can be in any format. This is just the ideal example of what it should look like.

User

If you look through the schema.sql and the iam_users table, you will notice that they do not contain passwords because users can only log in through an OAuth2 provider such as Google and Discord.

Resetting Passwords, Emails etc;

If you look at the iam_users table again, you will notice a security_token and security_stamp field. ASP.NET CORE heavily inspired this. I wasn’t entirely sure how they did it, but I know whenever the security_stamp gets changed, the security_token gets invalidated. Right now I’m still trying to figure out how I would go about verifying it, but I have an idea, but before I tell you that I should break down how it exactly works at this current moment.

security_stamp

The model for the security_stamp doesn’t have a model but instead just takes String type.

In order to generate a valid security_stamp the function fn generate_security_stamp(security_stamp: String, action: &str);

This function simply gets the current time in milliseconds then hashes it with Sha256 then turns it into a hexadecimal string.

security_token

The model for the security_token is

#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
// security token for the user...
pub struct SecurityToken {
    pub token: String,
    pub expiry: u128,
    pub action: String,
}
  1. First we call SecurityToken::new(security_stamp: String, action: &str);

  2. Then we serialize with the model with serde_json then we convert the serialization into a hexadecimal.

  3. After we have the hexadecimal of the given model then we can store that into the database.

In order to grab the value we need to call decode_then_deserialize(security_token: Option<String>

This function decodes the hexadecimal string, then deserializes it and casts the generic type SecurityToken. If the token is empty, it returns None.

Shorthand

The function you would use to generate the actual token with the specified action is

UserSecurity::create(action); so if you would like to create an email reset you would do UserSecurity::create(“email_reset”); then in your route /auth/email_reset you would check if the SecurityToken exists and check if action is set to “email_reset” if not, deny access.

This is simple technical breakdown of the current status of my project.

I hate followers.

I hate followers. People who readily agree with someone because they're a role model, leader, or popular, regardless of whether they're right or wrong, then belittle, ignore, hate, and laugh at the people who go against them. I have been on both sides: people who follow me and people who criticized me for going against someone who is "above?" me. Anyways, if you want to easily make friends become a follower.

Been feeling off.

I haven't been myself these past two weeks, which is unusual for me. I'm unable to get any quality sleep. I wake up with my eyes feeling heavy and go to bed with them feeling heavy. It's the feeling of uncertainty for my future. I'm 24 and have achieved nothing.

Almost bought a Tesla.

I drive a 2016 Lexus ES 350. I am thankful I haven't had any problems with the car despite buying it used, I expect nothing less from a Toyota. The car itself is paid off (thanks, dad) and all I do is pay the insurance on the car. Luckily, he also performs the maintenance on my car as well (very thankful).

Now, because I enjoy writing code and the fact that the year is 2024 automatically makes me a tech guru, I have always been interested in buying a Tesla because of its technology. I was contemplating whether or not to buy one for a little over a year. I originally was going to buy the Model 3, but then Elon Musk dropped the 0.99% APR for the Model Y, and I decided I was going to get the Model Y at that moment. 

So, before I get into why I didn't go through with it, let me give you a little backstory about me: I'm 24, never went to college, learned to write software by myself, never went to one of those coding boot camps, and currently work at a minimum wage job (I am not going to specify the position and where because I am too embarrassed). If I had gone through buying it, I could have gotten $19,500 from Carvana for my Lexus. Now because I work at a shitty job, my dad would have had to co-sign the car with me, so $47000 - $19500 = $27500 - $7500 (federal tax credit) = $20000, so I would roughly have to take about a $20k loan (technically $27500). I would probably have to pay about ~$400 monthly, give or take. 

In the days leading up to going to the dealership (we went yesterday, May 13th), a lot of things were bothering me, and I was emotionally overwhelmed (not going to say why). But everything felt off to me when we went to the dealership that day. It just didn't feel right. We made our reservation for the Model Y at the dealership, and finished some of the required steps on the Tesla app. We went home, I went to my room and just felt so overwhelmed not the good kind either. Something didn't feel right, I went to the living room, and my dad started doing the paperwork online so we could sell my car to Carvana. Luckily, the app you need to use to take photos of your car wasn't working correctly, and I just told my dad that we should cancel the reservation. We did, and I felt a little bit of relief. I was upset, not because I didn't get the car but because I was about to go through with it. It didn't feel right, it felt wrong. 

I'm just so tired.