Policies
Policies are a group of authorization checks that concern one model or struct in particular. They traditionally map directly to CRUD actions, meaning you generally should define policies for all of your particular models/resources. They can be extended beyond traditional CRUD operations as well.
When you define a form or table page apart of a resource, those pages will check for your model's policy file to understand what the currently logged in user can perform. If none are registered, it will allow all operations.
For example, you can set certain conditions based on the currently logged in user to prevent them from, say, deleting a certain model like Appointments if their role is not admin.
Defining a policy
To create a new policy, we recommend creating a policy directory in your application. Best Practicies.
Each policy file direclty relates to one particular model. As an example, if your application has an appointment model (with a list of appointments, create/edit/view page, etc.) you'll want to create a appointment_policy.go file.
# create a policy directory inside your Iridium directory
mkdir iridium/policies
# create a new policy file
touch user_policy.goInside your policy file, you can define any number of functions attached to a policy struct to control certain permissions.
Each policy method must at a minimum accept Iridium's context.CustomContext interface.
Below, we'll show an example of defining & registering a policy for a custom appointment model defined in theAppointment struct. ?
// Your policy struct has no content, it's solely used to organize
// related methods
type AppointmentPolicy struct {}
// A function that defines whether users can create a particular appointment
func (_ AppointmentPolicy) Create(ctx context.CustomContext) bool {
// As an example, only users with an id of `1` can create apppointments.
return ctx.User().GetId() == 1
}Policy methods also allow any number of arguments. This is particularly useful when defining permission checks like edit.
func (_ AppointmentPolicy) Edit(ctx context.CustomContext, appointment *Appointment) bool {
// Users can only edit an appointment if they are the owner of that appointment
if appointment.User.Id == ctx.User().GetId() {
return true
}
return false
}Additionally, you can provide information about the policy check if you want greater insight why it passed or failed elsewhere in your application
func (_ AppointmentPolicy) Delete(ctx context.CustomContext, appointment *Appointment) auth.Response {
if appointment.User.Id == ctx.User().GetId() {
return auth.Allow()
}
return auth.Deny("You must be an appointment's owner to delete it")
}Registering Policies
You'll need to register all of your policy files with their underlying type when booting iridium.
You can do this by creating a method in your main statement to organize each policy registration and calling that before serving Iridium. If you'd prefer, you can also include init statements in each policy file to register them as they are used. The choice is yours.
INFO
All three of these methods are functionally equivalent. Whatever you prefer
Register a policy
You can register a new policy as using the RegisterPolicy method as so:
import "github.com/iridiumgo/iridium/bootstrap/auth/policy"
policy.RegisterPolicy(&Appointment, &AppointmentPolicy{})Register multiple policies
You can register multiple policies at once using RegisterPolicies
import "github.com/iridiumgo/iridium/bootstrap/auth/policy"
policy.RegisterPolicies(map[interface{}]policy.Policy{
models.Post{}: &policies.PostPolicy{},
models.Comment{}: &policies.CommentPolicy{},
models.User{}: &policies.UserPolicy{},
})Register a typed policy
If you'd prefer to register a policy using types rather than passing empty instantiations, you can do so as:
import "github.com/iridiumgo/iridium/bootstrap/auth/policy"
policy.RegisterTypedPolicy[Appointment, AppointmentPolicy]()Common Authorization Methods
Here is a list of common methods generally found in policy files. Please note, none of these are required - you can create policy methods with your own names and implementation and call them throughout Iridium
View<- Determine if a user can view a particular modelViewAny<- Authorize a user view any of a particular modelCreate<- Authorize a user to create a modelEdit<- Authorize a user to edit a modelDelete<- Authorize a user to delete a model. UsingForceDeletecan let you distinguish between soft and hard deletes.Restore<- Authorize a user to restore a soft-deleted modelForceDelete<- Authorize a user to permanently delete a model
Calling a policy
You can call a policy directly or through your gate syntax.
Via Gates
By default, whenever you call a gate with a particular name, Iridium first checks if a policy exists for your target model with the same name. If one does, it will use that policy check instead of your gate. If a policy file doesn't exist, no target is provided, or your policy does not have a matching name, it will then search for a matching gate.
Gate calls rely on the first argument to determine which policy to match. So if you have an appointment policy and you provide an appointment model as your gate's first argument, it will search if a policy exists for that appointment.
// create takes an empty &Appointment to match to your appointment policy
gate.Call(ctx, "create", &Appointment{}) // Check if you can create an appointment
// Here you're providing an appointment with values to your appointment policy's
// edit method.
gate.Call(ctx, "edit", &Appointment{
Id : 1,
Name : "My Appointment",
})Call Policies Directly
If you'd prefer not to use the gate syntax to call your policies in order to be more explicit, you can call your policy file methods directly via CallPolicyMethod:
// Here we're calling the "create" method on the Appointment Policy.
// We need to provide the current ctx, and then can provide a list of arguments.
policy.CallPolicyMethod(&AppointmentPolicy{}, "create", ctx, arguments...)Instantiate policies
Of course, you can also directly instantiate your policy struct and call its methods as well:
passed := new(AppointmentPolicy).Create(ctx, arguments....)If I can directly instantiate policies, why use the other methods that rely on reflection?
To be clear this is a completely reasonable take for interacting with policies.
However, the power of policy files most comes from getting authentication for free, and having them work with gates.
- Iridium's default page hooks like CreatePage, EditPage, etc. attempt to call your policies for you behind the scences to save you work and avoid forgetting to add authorization hooks everywhere.
- Policies and gates are supposed to work together. When you use a gate hook, to check if a user can perform an action, you're also checking their policies for free in a single call.
However, how you use policies and gates is your own choice to make. Iridium's policies and gates are inspired by Laravel's authentication workflow, but it's your own choice whether to use them at all.
Policy Methods
Iridium's policies have a few additional methods for controlling and interacting with your authorization system: