in api an instance of a model.Model
is called a resource. Resources need to be identified in your api. This is done in two ways:
urn/home/user/joebloggs
u-0123456789abcdef
api needs work out whether to allow a call which uses a resource. To support this each resource must be owned by a User
. The default behvaiour says the owner and superusers can use a resource, but nobody else. This behaviour can be overridden in the resource's code - override allows_api_check_user()
, which is a good place, for example, to allow public read access. Alternatively, you could create a Role
to access the resource and setting this Role
as a parent to a Role
of the other user - this is a good way, for example, to allow elevated access for a particular task.
An api id is derived from a resource's pk
. The pk
is encrypted to make the id. The settings API_ID_ENCRYPTION_KEY
and API_ID_INITIALISATION_VECTOR
set how the pk
is encrypted. By default, pk
is assumed to be a BigIntegerField
- the default for Django. To support other forms of pk
, override pk_as_bytes()
and pk_from_bytes()
.
You add this mixin to your model.Model
s, It needs configuration from your model - see setup. It provides these:
.urn
.api_id
api_find(id:str)->cls
classmethodcls
given an id, which can be a urn id or a api id. Raises cls.DoesNotExist
if that resource could not be found.Add api's mixin to your model and define these:
class Author(models.Model, api.Mixin):
# api support...
class API:
urn_prefix = 'writing/author'
api_id_prefix = 'a-'
textname = 'author'
@property
def owning_user(self):
return self.owner
@property
def urn_id(self):
return f'{self.fullname}'
@staticmethod
def urn_find(fullname):
return Author.objects.get(fullname=fullname)
# ...api support
.API
This class provides configuration for the api for your Model
.API.urn_prefix
urn/home/user/joebloggs
it's home/user
. These must be unique..API.api_id_prefix
u-0123456789abcdef
it's the u
. These must be unique..API.textname
.owning_user
Role
allows it and everybody else is not allowed to do anything. This is the default behaviour which can be overidden..urn_id
urn/home/user/joebloggs
it is joebloggs
, and in urn/home/user/joe/bloggs
it is joe/bloggs
:def urn_id(self):
return self.fullname
or
def uri_id(self):
return self.firstname + '/' + self.lastname
.urn_find()
@staticmethod
def urn_find(fullname):
return Author.objects.get(fullname=fullname)
or
@staticmethod
def urn_find(firstname, lastname):
return Author.objects.get(firstname=firstname, lastname=lastname)
.policies
optional When present, this is expected to be a ManyToManyField
like this:
policies = models.ManyToManyField(
Policy,
blank=True,
related_name='mymodels',
related_query_name='mymodel',
through='MyModelUsesPolicy',
through_fields=('resource', 'policy', )
)
where MyModelUsesPolicy
is like this:
from api.models import ResourceUsesPolicy
class MyModelUsesPolicy(ResourceUsesPolicy(MyModel)):
pass
This allows policies to be attached to your resource (in the example called MyModel). A Policy
used in this way
has the usual context, plus:
resource.api_id
resource.urn
resource.owning_user
resource. ...
for_context()
The through
model
allows additional context to be provided to the policy when it is deciding whether to allow a particular api
call. ResourceUsesPolicy(cls)
is a class generator. It generates an abstract models.Model
with the required fields.
That is the bare essentials for setting up a policies
field - you may want to allow your users to adjust their
resources' policies
lists.
The following is optional, but shows what can also be achieved:
Your users may also want to see how many times they've used different Policy
s. The real models's classes are kept
in a list, ResourceUsesPolicyBase.derived_classes
. Here's some suggested extra attributes to help with counting
how many times a Policy
has been used for what:
class MyModelUsesPolicy(ResourceUsesPolicy(MyModel)):
# which group Policy will be tallied against
tally = 'resources'
@classmethod
def query_for_owned_by(cls, owner):
return Q(resource__owner_user=owner)
tally
Policy
s been used for.query_for_owned_by
.objects.filter()
. The ForeignKey
field for your resource is called resource
and
the resurned Q-expression should filter your resources down to those owned by owner
.These could be used like this:
tallies = defaultdict(lambda: 0)
for cls in ResourceUsesPolicyBase.derived_classes:
tallies[cls.tally] += cls.objects.filter(policy=policy).filter(cls.query_for_owned_by(user)).count()
Then, for example, tallies['resources']
is the number of time policy
has been used by one of your resources.
.allows_api_check_user
Checks the user to see whether they can access your object. Thie default implementation allows access to super users and the owner. In this override, a public flag is checked:
def allows_api_check_user(self, ctx: PolicyContext):
# allow public repos read access to anybody
if ctx.access == ResourceAccess.Read and self.public:
return True
return super().allows_api_check_user(ctx)
PolicyContext
has:
role
Role
being checkedaction
resources
access
ResourceAccess
enum.context
kwargs
:From an api call:
["source"] - 'session' (web page call) or 'auth' (from authorization)
["originator"]["ip"] - `.META['REMOTE_ADDR']` from the request
["useragent"] - `.META['HTTP_USER_AGENT']` from the request
From all calls (api and site internal):
["api"]["principal"]["id"] - api_id of the calling user
["api"]["principal"]["username"] - username of the calling user
["api"]["principal"]["urn"] - urn of the calling user
["currenttime"] - datetime of the calling time
Other values may be set by the Role
and method. When substituting in Policy
s, use 'dot form':
{source}
{originator.ip}
{useragent}
{api.principal.id}
{api.principal.username}
{api.principal.urn}
{currenttime}
.pk_as_bytes
and .pk_from_bytes
pk
is not a BigIntegerField
, you must define both of these methods. These methods convert
your primary key from being a bytes string whose length must be a multiple of 8, and your primary key.This is the default implementation:
def pk_as_bytes(self):
return self.pk.to_bytes(8, 'big')
@staticmethod
def pk_from_bytes(b):
return int.from_bytes(b, "big")
or, in this example, the pk is a string which never has a '\x00' in it:
def pk_as_bytes(self):
r = self.pk.encode()
l = len(r)
r += b'\x00' * ((l+7)//8*8-l)
return r
@staticmethod
def pk_from_bytes(b):
return b.replace(b'\x00', b'').decode()
.for_context(self)
When your model is available to policies, such when it is passes as a method argument, this method gives the values are available to the policy. The default implementation is:
class YourModel..
...
def for_context(self):
return {
'api_id': self.api_id,
'urn': self.urn,
'owning_user': self.owning_user.for_context(),
}
so, by default, if the argument is thing
:
.mything.api_id
.mything.urn
.mything.owning_user
Only expose values you are certain are safe, in particular do not expose a model itself - always
use its .for_context()
instead, as seen above.
The returned dictionary is passed through attrdict - if you need a dictionary to stay as a dictionary it will need to be tagged - refer to the attrdict documentation.