Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Innkeeper Auth Key CRUD #740

Merged
merged 4 commits into from
Aug 1, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading