Friday, 31 July 2026

The Door Is Not the Role

 



Most permission problems begin with an innocent idea: ‘We’ll give this kind of person that kind of access.’ It is tidy, fast, and has the satisfying clunk of a drawer closing. It also tends to fail the moment a real person has more than one responsibility, changes teams, covers a colleague, or simply behaves like a human being rather than a row in a spreadsheet.

A sturdier approach is to test access through journeys and capabilities. In plain English: start with what someone is trying to accomplish, then verify the smallest set of permissions that makes that task possible—no more, no less. This is not glamorous. Neither is a seatbelt. Both are rather more useful when the road gets interesting.

Start with a journey, not a label

A role is a shorthand. A journey is a story. ‘Administrator’ tells you almost nothing about the exact work being done; ‘review a request, correct a record, and hand it off’ tells you where to look for friction and risk.

For each important journey, write a small scenario with a beginning, middle, and end. Keep it concrete: what is the person trying to achieve, what information should they see, what action should they be able to take, and what must remain out of reach? The final question is not pessimism. It is just how the universe keeps its filing cabinet from eating the cat.

Build a capability map

Instead of beginning with a long list of job titles, name the capabilities your product actually needs. Common examples include viewing a record, editing a record, approving a change, inviting another person, exporting data, or changing billing details. A person may need several of these; a role may bundle some of them; but the capability remains the useful unit of truth.

  • Name actions in verbs: view, create, edit, approve, invite, export, manage.
  • Separate access to see something from access to change it.
  • Treat high-impact actions—money movement, deletion, publishing, user management—as their own capabilities.
  • Make temporary or exceptional access visible and easy to remove.

Test the edges where bugs prefer to live

The happy path is necessary, but it is also where systems are most charmingly well behaved. The value is in testing the boundary conditions: a person who can view but not edit; someone whose access changes mid-journey; a delegated helper; a person with two overlapping responsibilities; an account that should lose a capability immediately.

A compact test matrix can keep this practical. For every journey, include at least one expected success and one expected refusal. If a refusal is vague, confusing, or quietly bypassable, it is telling you something useful about the design—not merely about the test.

Make refusals useful

A blocked action is not automatically a failure. Sometimes it is the system doing its job with admirable restraint. The experience matters, though. A useful refusal says what happened in ordinary language, avoids revealing sensitive information, and gives the person a sensible next step. ‘You cannot do that’ is technically accurate in the way a locked shed is technically architecture.

Where possible, show people the boundary before they collide with it. Hide or disable actions they cannot use, explain why only when it is safe to do so, and offer a route to the right owner or process. Clarity reduces support requests; mystery merely relocates them.

Keep the test set alive

Permission models drift as products grow. New features add new actions, teams change shape, and one ‘small exception’ wanders off to breed in the woods. Turn the journeys that matter most into repeatable checks. Revisit them when capabilities change, not only after an incident has already delivered its tiny brass band.

The goal is not a perfectly clever access model. It is a comprehensible one: people can do the work they are meant to do, they cannot quietly do the work they are not, and everyone can explain why. That is a rare form of calm in software—and worth cultivating.

Final takeaway

Test permissions through the real journeys people take and the capabilities they need. Roles are useful labels; journeys reveal whether the doors actually lead anywhere.

Friday, 24 February 2023

SlackAPIPostOperator unable to send to private slack channel.

 
I am using SlackAPIPostOperator from an Airflow Dag.  And have successfully made a bot that will send messages to my Slack Channel 

slack_post = SlackAPIPostOperator(
task_id="slack_notification",
channel="#my_slack_channel",
username="airflow",
text="A test message from Airflow. Please ignore",
token="#######",
dag=dag,
)


However, when I use the same logic on a Private channel it wouldn't work.  And I couldn't see my bot to be able to 'invite' it to the channel.  


To fix this ...

To grant your bot permission to join private channels, you will need to ensure that it has the channels:join and channels:read OAuth scopes. Here’s how you can check and modify your bot’s scopes:
  1. Go to the Slack API website and navigate to the “OAuth & Permissions” section of your app’s settings.
  2. Scroll down to the “Scopes” section and check that your bot has the channels:join and channels:read scopes.
  3. If the scopes are not already added, click on the “Add an OAuth Scope” button and select the scopes from the dropdown list.
  4. After adding the scopes, make sure to re-install your app in the workspace to apply the changes.
After , that I had to re-invite the Bot to the Private Channel  ( through the add people ) and it worked.

Wednesday, 4 January 2023

In Wagtail GeoLocation Widget how to set a Default location ( and set zoom )

 To set the default map location in the wagtailgeowidget package, you can define the GEO_WIDGET_DEFAULT_LOCATION setting in your Django settings file.

Here is an example of how to do this:

GEO_WIDGET_DEFAULT_LOCATION = {"lat": 50.5230, "lng": -4.6558}

This will set the default map location to the coordinates 50.5230° N, 4.6558° W (Bodmin, Cornwall, United Kingdom).

The GEO_WIDGET_DEFAULT_LOCATION setting should be a dictionary with keys 'lat' and 'lng' representing the latitude and longitude, respectively. The values should be float values.

You can also set the default map location dynamically based on the value of a field in your model by defining a callable as the value of the GEO_WIDGET_DEFAULT_LOCATION setting.

Here's an example of how to do this:

from django.conf import settings def get_default_location(): # Code to retrieve default location from model goes here return {"lat": 50.5230, "lng": -4.6558} GEO_WIDGET_DEFAULT_LOCATION = get_default_location

This will set the default map location to the value returned by the get_default_location function.

Tuesday, 31 May 2022

Django - Grabbing a JSON object from QueryDict

 
What I'm doing here is passing some JSON through the 'rest.framework' .  The problem is that when using 'request.json'  I'm seeing the following being returned. 

<QueryDict: {'': ['{"time":{"0":"06:00","1":"07:00","2":"08:00","3":"09:00","4":"10...


What I need to do is extract the JSON out .  To do this , the following works. 

list(data.values())[0]




Wednesday, 25 May 2022

M1 Chip issue with AWS Lambda SAM Layers

 

Just a quick note , if you've searched and found this blog then I expect you already know deep down that the M1 chip is your issue.  There is a solution though.  Let me explain. 


So, what we've been doing is using Lambda Layers to install Python packages on our Lambda installation.  However, when putting the same code on my Mac ( M1 Chip ) it doesn't work ! 

https://docs.aws.amazon.com/lambda/latest/dg/configuration-layers.html

It does work if we take the zip file from the AMD64 chipped Mac , and put that on my Mac and proceed. 

That is the solution we're having to use at the moment , the zip needs to be compiled on the AMD64 chip , doing it on the M1 chip won't work. 


Tuesday, 17 May 2022

Could not 'pipenv install email' - 'python setup.py egg_info did not run successfully.' error.

Here's the error code I was getting. 

Installing email... Error: An error occurred while installing email! Error text: Collecting email Using cached email-4.0.2.tar.gz (1.2 MB) Preparing metadata (setup.py): started Preparing metadata (setup.py): finished with status 'error' error: subprocess-exited-with-error × python setup.py egg_info did not run successfully. │ exit code: 1 ╰─> [1 lines of output] ERROR: Can not execute `setup.py` since setuptools is not available in the build environment. [end of output] note: This error originates from a subprocess, and is likely not a problem with pip. error: metadata-generation-failed × Encountered error while generating package metadata. ╰─> See above for output. note: This is an issue with the package mentioned above, not pip. hint: See above for details. This is likely caused by a bug in email. Report this to its maintainers. ✘ Installation Failed



The recommended solution across the web seemed to be to run the following. 

pipenv install --upgrade setuptools


However, this didn't work for me.  I needed to use the '--pre' tag. 

pipenv install --pre email


This now works, the version I had the issue with was 'email-4.0.2'


So, if you have a 'email-4.0.2 won't pipenv install' issue then I hope this post helps. 




Thursday, 7 April 2022

Wordpress installation on Docker - How to access the Database

 After running through this blog on how to create a Wordpress site on Docker.  ( Which works excellently ) https://davidyeiser.com/tutorials/docker-wordpress-theme-setup


After doing so, I couldn't quite work out what details I needed to use in Sequel Pro / Sequel Ace. 



There's a line that I needed to add to the Wordpress DB setup though, and that was to add the ports .

ports:
- "3366:3306"


So my entire file looks like this. 

version: '3'

services:
db:
image: mysql:5.7
volumes:
- db_data:/var/lib/mysql
ports:
- "3366:3306"
restart: always
environment:
MYSQL_ROOT_PASSWORD: somewordpress
MYSQL_DATABASE: wordpress
MYSQL_USER: wordpress
MYSQL_PASSWORD: wordpress

wordpress:
depends_on:
- db
image: wordpress:5.1.1-php7.3-apache
ports:
- "8000:80"
restart: always
environment:
WORDPRESS_DB_HOST: db:3306
WORDPRESS_DB_USER: wordpress
WORDPRESS_DB_PASSWORD: wordpress
working_dir: /var/www/html
volumes:
- ./wp-content:/var/www/html/wp-content
- ./uploads.ini:/usr/local/etc/php/conf.d/uploads.ini
volumes:
db_data:


And then I can use