Skip to content
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
3 changes: 0 additions & 3 deletions auth-api/migrations/versions/9c58b78727c8_users_type.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,6 @@ def upgrade():
login_source = user.login_source
if user.login_source in [LoginSource.BCEID.value, LoginSource.BCSC.value]:
user_type = Role.PUBLIC_USER.name
elif user.login_source == LoginSource.BCROS.value or user.username.startswith("bcros/"):
user_type = Role.ANONYMOUS_USER.name
login_source = LoginSource.BCSC.value
elif user.login_source == LoginSource.STAFF.value:
user_type = Role.STAFF.name

Expand Down
14 changes: 4 additions & 10 deletions auth-api/src/auth_api/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -119,12 +119,6 @@ class _Config: # pylint: disable=too-few-public-methods
ENTITY_SVC_CLIENT_ID = os.getenv("ENTITY_SVC_CLIENT_ID")
ENTITY_SVC_CLIENT_SECRET = os.getenv("ENTITY_SVC_CLIENT_SECRET")

# Upstream Keycloak setting - should be removed
KEYCLOAK_BCROS_BASE_URL = os.getenv("KEYCLOAK_BCROS_BASE_URL")
KEYCLOAK_BCROS_REALMNAME = os.getenv("KEYCLOAK_BCROS_REALMNAME")
KEYCLOAK_BCROS_ADMIN_CLIENTID = os.getenv("KEYCLOAK_BCROS_ADMIN_CLIENTID")
KEYCLOAK_BCROS_ADMIN_SECRET = os.getenv("KEYCLOAK_BCROS_ADMIN_SECRET")

# API Endpoints
BCOL_API_URL = os.getenv("BCOL_API_URL", "") + os.getenv("BCOL_API_VERSION", "")
NAMEX_API_URL = os.getenv("NAMEX_API_URL", "") + os.getenv("NAMEX_API_VERSION", "")
Expand Down Expand Up @@ -277,10 +271,10 @@ class TestConfig(_Config): # pylint: disable=too-few-public-methods
4H8UZcVFN95vEKxJiLRjAmj6g273pu9kK4ymXNEjWWJn
-----END RSA PRIVATE KEY-----"""

KEYCLOAK_ADMIN_USERNAME = KEYCLOAK_BCROS_ADMIN_CLIENTID = os.getenv("KEYCLOAK_TEST_ADMIN_CLIENTID")
KEYCLOAK_ADMIN_SECRET = KEYCLOAK_BCROS_ADMIN_SECRET = os.getenv("KEYCLOAK_TEST_ADMIN_SECRET")
KEYCLOAK_BASE_URL = KEYCLOAK_BCROS_BASE_URL = os.getenv("KEYCLOAK_TEST_BASE_URL")
KEYCLOAK_REALMNAME = KEYCLOAK_BCROS_REALMNAME = os.getenv("KEYCLOAK_TEST_REALMNAME")
KEYCLOAK_ADMIN_USERNAME = os.getenv("KEYCLOAK_TEST_ADMIN_CLIENTID")
KEYCLOAK_ADMIN_SECRET = os.getenv("KEYCLOAK_TEST_ADMIN_SECRET")
KEYCLOAK_BASE_URL = os.getenv("KEYCLOAK_TEST_BASE_URL")
KEYCLOAK_REALMNAME = os.getenv("KEYCLOAK_TEST_REALMNAME")
JWT_OIDC_AUDIENCE = os.getenv("JWT_OIDC_TEST_AUDIENCE")
JWT_OIDC_CLIENT_SECRET = os.getenv("JWT_OIDC_TEST_CLIENT_SECRET")
JWT_OIDC_ISSUER = os.getenv("JWT_OIDC_TEST_ISSUER")
Expand Down
3 changes: 1 addition & 2 deletions auth-api/src/auth_api/exceptions/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,8 +82,7 @@ class Error(Enum):
ORG_CANNOT_BE_DISSOLVED = "Organization cannot be dissolved", HTTPStatus.NOT_ACCEPTABLE
FAILED_ADDING_USER_IN_KEYCLOAK = "Error adding user to keycloak", HTTPStatus.INTERNAL_SERVER_ERROR
ACCCESS_TYPE_MANDATORY = "staff created orgs needs access type", HTTPStatus.BAD_REQUEST
USER_CANT_CREATE_ANONYMOUS_ORG = "Only staff can create anonymous org", HTTPStatus.UNAUTHORIZED
USER_CANT_CREATE_GOVM_ORG = "Only staff can create govt ministy org", HTTPStatus.UNAUTHORIZED
USER_CANT_CREATE_GOVM_ORG = "Only staff can create govt ministry org", HTTPStatus.UNAUTHORIZED

USER_CANT_CREATE_EXTRA_PROVINCIAL_ORG = (
"Only out of province users can create extra provincial org",
Expand Down
2 changes: 1 addition & 1 deletion auth-api/src/auth_api/exceptions/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
status_code - where possible use HTTP Error Codes
"""

from auth_api.exceptions.errors import Error # noqa: I001, I003
from auth_api.exceptions.errors import Error # noqa: I001


class BusinessException(Exception): # noqa: N818
Expand Down
28 changes: 7 additions & 21 deletions auth-api/src/auth_api/models/org.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ class Org(Versioned, BaseModel): # pylint: disable=too-few-public-methods,too-m
status_code = Column(ForeignKey("org_statuses.code"), nullable=False)
name = Column(String(250), index=True)
branch_name = Column(String(100), nullable=True, default="") # used for any additional info as branch name
access_type = Column(String(250), index=True, nullable=True) # for ANONYMOUS ACCESS
access_type = Column(String(250), index=True, nullable=True)
decision_made_by = Column(String(250))
decision_made_on = Column(DateTime, nullable=True)
bcol_user_id = Column(String(20))
Expand Down Expand Up @@ -259,16 +259,9 @@ def _search_for_statuses(cls, query, statuses):
.outerjoin(Invitation, Invitation.id == InvitationMembership.invitation_id)
.filter(Invitation.invitation_status_code == InvitationStatus.PENDING.value)
.filter(
(
(Invitation.type == InvitationType.DIRECTOR_SEARCH.value)
& (Org.status_code == OrgStatusEnum.ACTIVE.value)
& (Org.access_type == AccessType.ANONYMOUS.value)
)
| (
(Invitation.type == InvitationType.GOVM.value)
& (Org.status_code == OrgStatusEnum.PENDING_INVITE_ACCEPT.value)
& (Org.access_type == AccessType.GOVM.value)
)
(Invitation.type == InvitationType.GOVM.value)
& (Org.status_code == OrgStatusEnum.PENDING_INVITE_ACCEPT.value)
& (Org.access_type == AccessType.GOVM.value)
)
)
query = query.filter(Org.id.notin_(pending_inv_subquery))
Expand All @@ -284,16 +277,9 @@ def search_pending_activation_orgs(cls, name: str):
.options(contains_eager(Org.invitations).load_only(InvitationMembership.invitation_id))
.filter(Invitation.invitation_status_code == InvitationStatus.PENDING.value)
.filter(
(
(Invitation.type == InvitationType.DIRECTOR_SEARCH.value)
& (Org.status_code == OrgStatusEnum.ACTIVE.value)
& (Org.access_type == AccessType.ANONYMOUS.value)
)
| (
(Invitation.type == InvitationType.GOVM.value)
& (Org.status_code == OrgStatusEnum.PENDING_INVITE_ACCEPT.value)
& (Org.access_type == AccessType.GOVM.value)
)
(Invitation.type == InvitationType.GOVM.value)
& (Org.status_code == OrgStatusEnum.PENDING_INVITE_ACCEPT.value)
& (Org.access_type == AccessType.GOVM.value)
)
)
if name:
Expand Down
11 changes: 3 additions & 8 deletions auth-api/src/auth_api/models/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,13 +52,13 @@ class User(Versioned, BaseModel):
"keycloak_guid",
UUID(as_uuid=True),
unique=True,
nullable=True, # bcros users comes with no guid
nullable=True,
)

is_terms_of_use_accepted = Column(Boolean(), default=False, nullable=True)
terms_of_use_accepted_version = Column(ForeignKey("documents.version_id"), nullable=True)

# a type for the user to identify what kind of user it is..ie anonymous , bcsc etc ..similar to login source
# a type for the user to identify what kind of user it is..bcsc etc ..similar to login source
type = Column("type", String(200), nullable=True)
status = Column(ForeignKey("user_status_codes.id"))
idp_userid = Column("idp_userid", String(256), index=True)
Expand Down Expand Up @@ -272,12 +272,7 @@ def _get_type(cls, user_from_context: UserContext) -> str:
"""Return type of the user from the token info."""
user_type: str = None
if user_from_context.roles:
if (
Role.ANONYMOUS_USER.value in user_from_context.roles
or user_from_context.login_source == LoginSource.BCROS.value
):
user_type = Role.ANONYMOUS_USER.name
elif user_from_context.is_staff():
if user_from_context.is_staff():
user_type = Role.STAFF.name
elif Role.GOV_ACCOUNT_USER.value in user_from_context.roles:
user_type = Role.GOV_ACCOUNT_USER.name
Expand Down
2 changes: 0 additions & 2 deletions auth-api/src/auth_api/resources/v1/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@
from .activity_log import bp as activity_log_bp
from .affiliation_invitation import bp as affiliation_invitation_bp
from .bcol_profiles import bp as bcol_profiles_bp
from .bulk_user import bp as bulk_user_bp
from .codes import bp as codes_bp
from .documents import bp as documents_bp
from .documents_affidavit import bp as documents_affidavit_bp
Expand Down Expand Up @@ -60,7 +59,6 @@ def init_app(self, app):
self.app.register_blueprint(activity_log_bp)
self.app.register_blueprint(affiliation_invitation_bp)
self.app.register_blueprint(bcol_profiles_bp)
self.app.register_blueprint(bulk_user_bp)
self.app.register_blueprint(codes_bp)
self.app.register_blueprint(documents_bp)
self.app.register_blueprint(documents_affidavit_bp)
Expand Down
47 changes: 0 additions & 47 deletions auth-api/src/auth_api/resources/v1/bulk_user.py

This file was deleted.

8 changes: 2 additions & 6 deletions auth-api/src/auth_api/resources/v1/documents.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
from auth_api.services.google_store import GoogleStoreService
from auth_api.utils.auth import jwt as _jwt
from auth_api.utils.endpoints_enums import EndpointEnum
from auth_api.utils.enums import AccessType, DocumentType, LoginSource
from auth_api.utils.enums import DocumentType, LoginSource

bp = Blueprint("DOCUMENTS", __name__, url_prefix=f"{EndpointEnum.API_V1.value}/documents")

Expand All @@ -37,11 +37,7 @@ def get_document_by_type(document_type):
try:
if document_type == DocumentType.TERMS_OF_USE.value:
token = g.jwt_oidc_token_info
if token.get("accessType", None) == AccessType.ANONYMOUS.value:
document_type = DocumentType.TERMS_OF_USE_DIRECTOR_SEARCH.value
elif (
token.get("loginSource", None) == LoginSource.STAFF.value
): # ideally for govm user who logs in with IDIR
if token.get("loginSource", None) == LoginSource.STAFF.value: # ideally for govm user who logs in with IDIR
document_type = DocumentType.TERMS_OF_USE_GOVM.value

doc = DocumentService.fetch_latest_document(document_type)
Expand Down
2 changes: 1 addition & 1 deletion auth-api/src/auth_api/resources/v1/invitation.py
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ def accept_invitation_token(invitation_token):
response, status = (
InvitationService.accept_invitation(invitation_id, user, origin).as_dict(),
HTTPStatus.OK,
) # noqa:E127
)

except BusinessException as exception:
response, status = {"code": exception.code, "message": exception.message}, exception.status_code
Expand Down
14 changes: 2 additions & 12 deletions auth-api/src/auth_api/resources/v1/org.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@
from auth_api.services.flags import flags
from auth_api.utils.auth import jwt as _jwt
from auth_api.utils.endpoints_enums import EndpointEnum
from auth_api.utils.enums import AccessType, NotificationType, OrgStatus, OrgType, PatchActions, Status
from auth_api.utils.enums import NotificationType, OrgStatus, OrgType, PatchActions, Status
from auth_api.utils.role_validator import validate_roles
from auth_api.utils.roles import ( # noqa: I001
AFFILIATION_ALLOWED_ROLES,
Expand Down Expand Up @@ -148,10 +148,7 @@ def search_simple_orgs():

@bp.route("", methods=["POST"])
@cross_origin(origins="*")
@validate_roles(
allowed_roles=[Role.PUBLIC_USER.value, Role.STAFF_CREATE_ACCOUNTS.value, Role.SYSTEM.value],
not_allowed_roles=[Role.ANONYMOUS_USER.value],
)
@validate_roles(allowed_roles=[Role.PUBLIC_USER.value, Role.STAFF_CREATE_ACCOUNTS.value, Role.SYSTEM.value])
@_jwt.has_one_of_roles([Role.PUBLIC_USER.value, Role.STAFF_CREATE_ACCOUNTS.value, Role.SYSTEM.value])
def post_organization():
"""Post a new org using the request body.
Expand Down Expand Up @@ -194,17 +191,10 @@ def put_organization(org_id):
"""Update the org specified by the provided id with the request body."""
request_json = request.get_json()
valid_format, errors = schema_utils.validate(request_json, "org")
token_info = g.jwt_oidc_token_info
if not valid_format:
return {"message": schema_utils.serialize(errors)}, HTTPStatus.BAD_REQUEST
try:
org = OrgService.find_by_org_id(org_id, allowed_roles=(*CLIENT_ADMIN_ROLES, STAFF))
if (
org
and org.as_dict().get("accessType", None) == AccessType.ANONYMOUS.value
and Role.STAFF_CREATE_ACCOUNTS.value not in token_info.get("realm_access").get("roles")
):
return {"message": "The organisation can only be updated by a staff admin."}, HTTPStatus.UNAUTHORIZED
if org:
response, status = org.update_org(org_info=request_json).as_dict(), HTTPStatus.OK
else:
Expand Down
91 changes: 1 addition & 90 deletions auth-api/src/auth_api/resources/v1/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@
from auth_api.schemas import MembershipSchema, OrgSchema
from auth_api.schemas import utils as schema_utils
from auth_api.services import Affidavit as AffidavitService
from auth_api.services import Invitation as InvitationService
from auth_api.services.authorization import Authorization as AuthorizationService
from auth_api.services.keycloak import KeycloakService
from auth_api.services.membership import Membership as MembershipService
Expand All @@ -31,49 +30,12 @@
from auth_api.utils.auth import jwt as _jwt
from auth_api.utils.constants import GROUP_GOV_ACCOUNT_USERS
from auth_api.utils.endpoints_enums import EndpointEnum
from auth_api.utils.enums import LoginSource, Status
from auth_api.utils.enums import LoginSource
from auth_api.utils.roles import Role

bp = Blueprint("USERS", __name__, url_prefix=f"{EndpointEnum.API_V1.value}/users")


@bp.route("/bcros", methods=["POST", "OPTIONS"])
@cross_origin(origins="*", methods=["POST"])
def post_anonymous_user():
"""Post a new user using the request body who has a proper invitation."""
try:
request_json = request.get_json()
invitation_token = request.headers.get("invitation_token", None)
invitation = InvitationService.validate_token(invitation_token).as_dict()

valid_format, errors = schema_utils.validate(request_json, "anonymous_user")
if not valid_format:
return {"message": schema_utils.serialize(errors)}, HTTPStatus.BAD_REQUEST

membership_details = {
"email": invitation["recipient_email"],
"membershipType": invitation["membership"][0]["membership_type"],
"update_password_on_login": False,
}
membership_details.update(request_json)
user = UserService.create_user_and_add_membership(
[membership_details], invitation["membership"][0]["org"]["id"], single_mode=True
)
user_dict = user["users"][0]
if user_dict["http_status"] != HTTPStatus.CREATED:
response, status = (
{"code": user_dict["http_status"], "message": user_dict["error"]},
user_dict["http_status"],
)
else:
InvitationService.accept_invitation(invitation["id"], None, None, False)
response, status = user, HTTPStatus.CREATED

except BusinessException as exception:
response, status = {"code": exception.code, "message": exception.message}, exception.status_code
return response, status


@bp.route("", methods=["GET", "OPTIONS"])
@cross_origin(origins="*", methods=["GET", "POST"])
@_jwt.has_one_of_roles([Role.STAFF_VIEW_ACCOUNTS.value])
Expand Down Expand Up @@ -121,11 +83,6 @@ def post_user():
# Add the user to public_users group if the user doesn't have public_user group
if token.get("loginSource", "") != LoginSource.STAFF.value:
KeycloakService.join_users_group()
# For anonymous users, there are no invitation process for members,
# so whenever they login perform this check and add them to corresponding groups
if token.get("loginSource", "") == LoginSource.BCROS.value:
if len(OrgService.get_orgs(user.identifier, [Status.ACTIVE.value])) > 0:
KeycloakService.join_account_holders_group()
if user.type == Role.STAFF.name:
MembershipService.add_staff_membership(user.identifier)
else:
Expand Down Expand Up @@ -176,52 +133,6 @@ def get_by_username(username):
return response, status


@bp.route("/<path:username>", methods=["DELETE"])
@cross_origin(origins="*")
@_jwt.requires_auth
def delete_by_username(username):
"""Delete the user profile associated with the provided username."""
try:
user = UserService.find_by_username(username)
if user is None:
response, status = jsonify({"message": f"User {username} does not exist."}), HTTPStatus.NOT_FOUND
elif user.as_dict().get("type", None) != Role.ANONYMOUS_USER.name:
response, status = {"Normal users cant be deleted", HTTPStatus.NOT_IMPLEMENTED}
else:
UserService.delete_anonymous_user(username)
response, status = "", HTTPStatus.NO_CONTENT
except BusinessException as exception:
response, status = {"code": exception.code, "message": exception.message}, exception.status_code
return response, status


@bp.route("/<path:username>", methods=["PATCH"])
@cross_origin(origins="*")
@_jwt.requires_auth
def patch_by_username(username):
"""Patch the user profile associated with the provided username.

User only for patching the password.
"""
try:
request_json = request.get_json()
valid_format, errors = schema_utils.validate(request_json, "anonymous_user")
if not valid_format:
return {"message": schema_utils.serialize(errors)}, HTTPStatus.BAD_REQUEST
user = UserService.find_by_username(username)

if user is None:
response, status = jsonify({"message": f"User {username} does not exist."}), HTTPStatus.NOT_FOUND
elif user.as_dict().get("type", None) != Role.ANONYMOUS_USER.name:
response, status = {"Normal users cant be patched", HTTPStatus.NOT_IMPLEMENTED}
else:
UserService.reset_password_for_anon_user(request_json, username)
response, status = "", HTTPStatus.NO_CONTENT
except BusinessException as exception:
response, status = {"code": exception.code, "message": exception.message}, exception.status_code
return response, status


@bp.route("/@me", methods=["GET", "OPTIONS"])
@cross_origin(origins="*", methods=["GET", "PATCH", "DELETE"])
@_jwt.requires_auth
Expand Down
Loading