Skip to content

Commit

Permalink
Merge pull request #740 from bcgov/feature/authMethods
Browse files Browse the repository at this point in the history
Add Innkeeper Auth Key CRUD
  • Loading branch information
esune committed Aug 1, 2023
2 parents a512385 + e156cdd commit c4af61b
Show file tree
Hide file tree
Showing 3 changed files with 312 additions and 2 deletions.
112 changes: 112 additions & 0 deletions plugins/traction_innkeeper/traction_innkeeper/v1_0/innkeeper/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -370,3 +370,115 @@ class Meta:
fields.Str(description="Ledger id"),
required=False,
)


class TenantAuthenticationApiRecord(BaseRecord):
"""Innkeeper Tenant Authentication - API Record Schema"""

class Meta:
"""TenantAuthenticationApiRecord Meta."""

schema_class = "TenantAuthenticationApiRecordSchema"

RECORD_TYPE = "tenant_authentication_api"
RECORD_ID_NAME = "tenant_authentication_api_id"
TAG_NAMES = {
"tenant_id",
}

def __init__(
self,
*,
tenant_authentication_api_id: str = None,
tenant_id: str = None,
api_key_token_salt: str = None,
api_key_token_hash: str = None,
alias: str = None,
**kwargs,
):
"""Construct record."""
super().__init__(tenant_authentication_api_id, **kwargs)
self.tenant_id = tenant_id
self.api_key_token_salt = api_key_token_salt
self.api_key_token_hash = api_key_token_hash
self.alias = alias

@property
def tenant_authentication_api_id(self) -> Optional[str]:
"""Return record id."""
return uuid.UUID(self._id).hex

@classmethod
async def retrieve_by_auth_api_id(
cls,
session: ProfileSession,
tenant_authentication_api_id: str,
*,
for_update=False,
) -> "TenantAuthenticationApiRecord":
"""Retrieve TenantAuthenticationApiRecord by tenant_authentication_api_id.
Args:
session: the profile session to use
tenant_authentication_api_id: the tenant_authentication_api_id by which to filter
"""
record = await cls.retrieve_by_id(
session, tenant_authentication_api_id, for_update=for_update
)
return record

@classmethod
async def query_by_tenant_id(
cls,
session: ProfileSession,
tenant_id: str,
) -> "TenantAuthenticationApiRecord":
"""Retrieve TenantAuthenticationApiRecord by tenant_id.
Args:
session: the profile session to use
tenant_id: the tenant_id by which to filter
"""
tag_filter = {
**{"tenant_id": tenant_id for _ in [""] if tenant_id},
}

result = await cls.query(session, tag_filter)
return result

@property
def record_value(self) -> dict:
"""Return record value."""
return {
prop: getattr(self, prop)
for prop in (
"tenant_id",
"api_key_token_salt",
"api_key_token_hash",
"alias",
)
}

class TenantAuthenticationApiRecordSchema(BaseRecordSchema):
"""Innkeeper Tenant Authentication - API Record Schema."""

class Meta:
"""TenantAuthenticationApiRecordSchema Meta."""

model_class = "TenantAuthenticationApi"
unknown = EXCLUDE

tenant_authentication_api_id = fields.Str(
required=True,
description="Tenant Authentication API Record identifier",
example=UUIDFour.EXAMPLE,
)

tenant_id = fields.Str(
required=False,
description="Tenant Record identifier",
example=UUIDFour.EXAMPLE,
)

alias = fields.Str(
required=False,
description="Alias description for this API key",
)
Original file line number Diff line number Diff line change
Expand Up @@ -22,15 +22,18 @@
from . import TenantManager
from .utils import (
approve_reservation,
generate_reservation_token_data,
create_api_key,
ReservationException,
TenantApiKeyException,
TenantConfigSchema,
)
from .models import (
ReservationRecord,
ReservationRecordSchema,
TenantRecord,
TenantRecordSchema,
TenantAuthenticationApiRecord,
TenantAuthenticationApiRecordSchema
)

LOGGER = logging.getLogger(__name__)
Expand Down Expand Up @@ -207,6 +210,31 @@ class TenantIdMatchInfoSchema(OpenAPISchema):
)


class TenantAuthenticationsApiRequestSchema(OpenAPISchema):
"""Request schema for api auth record."""

tenant_id = fields.Str(
required=True,
description="Tenant ID",
example="000000-000000-00000-00000000",
)

alias = fields.Str(
required=False,
description="Optional alias/label",
example="API key for sample line of buisness",
)


class TenantAuthenticationsApiResponseSchema(OpenAPISchema):
"""Response schema for api auth record."""

reservation_id = fields.Str(
required=True,
description="The reservation record identifier",
example=UUIDFour.EXAMPLE,
)

class TenantListSchema(OpenAPISchema):
"""Response schema for tenants list."""

Expand All @@ -215,6 +243,28 @@ class TenantListSchema(OpenAPISchema):
description="List of tenants",
)

class TenantAuthenticationApiListSchema(OpenAPISchema):
"""Response schema for authentications - users list."""

results = fields.List(
fields.Nested(TenantAuthenticationApiRecordSchema()),
description="List of reservations",
)

class TenantAuthenticationApiIdMatchInfoSchema(OpenAPISchema):
"""Schema for finding a tenant auth user by the record ID."""
tenant_authentication_api_id = fields.Str(
description="Tenant authentication api key identifier", required=True, example=UUIDFour.EXAMPLE
)


class TenantAuthenticationApiOperationResponseSchema(OpenAPISchema):
"""Response schema for simple operations."""

success = fields.Bool(
required=True,
description="True if operation successful, false if otherwise",
)

@docs(
tags=["multitenancy"],
Expand Down Expand Up @@ -592,6 +642,105 @@ async def innkeeper_tenant_get(request: web.BaseRequest):
return web.json_response(rec.serialize())


@docs(tags=[SWAGGER_CATEGORY], summary="Create API Key Record")
@request_schema(TenantAuthenticationsApiRequestSchema())
@response_schema(TenantAuthenticationsApiResponseSchema(), 200, description="")
@innkeeper_only
@error_handler
async def innkeeper_authentications_api(request: web.BaseRequest):
context: AdminRequestContext = request["context"]

body = await request.json()
rec: TenantAuthenticationApiRecord = TenantAuthenticationApiRecord(**body)

# reservations are under base/root profile, use Tenant Manager profile
mgr = context.inject(TenantManager)
profile = mgr.profile

try:
api_key, tenant_authentication_api_id = await create_api_key(rec, mgr)
except TenantApiKeyException as err:
raise web.HTTPConflict(reason=str(err))

return web.json_response({"tenant_authentication_api_id": tenant_authentication_api_id, "api_key": api_key})


@docs(tags=[SWAGGER_CATEGORY], summary="List all API Key Records")
@response_schema(TenantAuthenticationApiListSchema(), 200, description="")
@innkeeper_only
@error_handler
async def innkeeper_authentications_api_list(request: web.BaseRequest):
context: AdminRequestContext = request["context"]

# records are under base/root profile, use Tenant Manager profile
mgr = context.inject(TenantManager)
profile = mgr.profile

tag_filter = {}
post_filter = {}
async with profile.session() as session:
# innkeeper can access all reservation records
records = await TenantAuthenticationApiRecord.query(
session=session,
tag_filter=tag_filter,
post_filter_positive=post_filter,
alt=True,
)
results = [record.serialize() for record in records]

return web.json_response({"results": results})


@docs(tags=[SWAGGER_CATEGORY], summary="Read API Key Record")
@match_info_schema(TenantAuthenticationApiIdMatchInfoSchema())
@response_schema(TenantAuthenticationApiRecordSchema(), 200, description="")
@innkeeper_only
@error_handler
async def innkeeper_authentications_api_get(request: web.BaseRequest):
context: AdminRequestContext = request["context"]
tenant_authentication_api_id = request.match_info["tenant_authentication_api_id"]

# records are under base/root profile, use Tenant Manager profile
mgr = context.inject(TenantManager)
profile = mgr.profile

async with profile.session() as session:
# innkeeper can access all tenants..
rec = await TenantAuthenticationApiRecord.retrieve_by_auth_api_id(session, tenant_authentication_api_id)
LOGGER.info(rec)

return web.json_response(rec.serialize())


@docs(tags=[SWAGGER_CATEGORY], summary="Delete API Key")
@match_info_schema(TenantAuthenticationApiIdMatchInfoSchema)
@response_schema(TenantAuthenticationApiOperationResponseSchema, 200, description="")
@innkeeper_only
@error_handler
async def innkeeper_authentications_api_delete(request: web.BaseRequest):
context: AdminRequestContext = request["context"]
tenant_authentication_api_id = request.match_info["tenant_authentication_api_id"]

# records are under base/root profile, use Tenant Manager profile
mgr = context.inject(TenantManager)
profile = mgr.profile

result = False
async with profile.session() as session:
rec = await TenantAuthenticationApiRecord.retrieve_by_auth_api_id(session, tenant_authentication_api_id)

await rec.delete_record(session)

try:
await TenantAuthenticationApiRecord.retrieve_by_auth_api_id(session, tenant_authentication_api_id)
except StorageNotFoundError:
# this is to be expected... do nothing, do not log
result = True

return web.json_response({"success": result})



async def register(app: web.Application):
"""Register routes."""
LOGGER.info("> registering routes")
Expand Down Expand Up @@ -637,6 +786,20 @@ async def register(app: web.Application):
"/innkeeper/tenants/{tenant_id}", innkeeper_tenant_get, allow_head=False
),
web.put("/innkeeper/tenants/{tenant_id}/config", tenant_config_update),
web.post("/innkeeper/authentications/api", innkeeper_authentications_api),
web.get(
"/innkeeper/authentications/api/",
innkeeper_authentications_api_list,
allow_head=False,
),
web.get(
"/innkeeper/authentications/api/{tenant_authentication_api_id}",
innkeeper_authentications_api_get,
allow_head=False,
),
web.delete("/innkeeper/authentications/api/{tenant_authentication_api_id}",
innkeeper_authentications_api_delete
),
]
)
LOGGER.info("< registering routes")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,11 @@
from aries_cloudagent.messaging.models.openapi import OpenAPISchema
from marshmallow import fields

from .models import ReservationRecord
from .models import (
ReservationRecord,
TenantAuthenticationApiRecord
)


from . import TenantManager

Expand Down Expand Up @@ -87,5 +91,36 @@ async def approve_reservation(
return _pwd


def generate_api_key_data():
_key = str(uuid.uuid4().hex)
LOGGER.info(f"_key = {_key}")

_salt = bcrypt.gensalt()
LOGGER.info(f"_salt = {_salt}")

_hash = bcrypt.hashpw(_key.encode("utf-8"), _salt)
LOGGER.info(f"_hash = {_hash}")

return _key, _salt, _hash


async def create_api_key(
rec: TenantAuthenticationApiRecord, manager: TenantManager
):
async with manager.profile.session() as session:
_key, _salt, _hash = generate_api_key_data()
rec.api_key_token_salt = _salt.decode("utf-8")
rec.api_key_token_hash = _hash.decode("utf-8")
await rec.save(session)
LOGGER.info(rec)

# return the generated key and the created record id
return _key, rec.tenant_authentication_api_id


class ReservationException(Exception):
pass


class TenantApiKeyException(Exception):
pass

0 comments on commit c4af61b

Please sign in to comment.