allow disabling authentication from the backend w/ burnettk

This commit is contained in:
jasquat 2023-05-25 12:00:24 -04:00
parent 01ec63f71a
commit 3f0f06817f
9 changed files with 91 additions and 55 deletions

View File

@ -14,13 +14,13 @@ fi
# shellcheck disable=2016 # shellcheck disable=2016
mysql -uroot "$database" -e ' mysql -uroot "$database" -e '
select u.username user, g.identifier group select u.username username, g.identifier group_name
FROM `user` u FROM `user` u
JOIN `user_group_assignment` uga on uga.user_id = u.id JOIN `user_group_assignment` uga ON uga.user_id = u.id
JOIN `group` g on g.id = uga.group_id; JOIN `group` g ON g.id = uga.group_id;
select pa.id, g.identifier group_identifier, pt.uri, permission from permission_assignment pa select pa.id, g.identifier group_identifier, pt.uri, permission from permission_assignment pa
join principal p on p.id = pa.principal_id JOIN principal p ON p.id = pa.principal_id
join `group` g on g.id = p.group_id JOIN `group` g ON g.id = p.group_id
join permission_target pt on pt.id = pa.permission_target_id; JOIN permission_target pt ON pt.id = pa.permission_target_id;
' '

View File

@ -79,6 +79,10 @@ SPIFFWORKFLOW_BACKEND_OPEN_ID_TENANT_SPECIFIC_FIELDS = environ.get(
"SPIFFWORKFLOW_BACKEND_OPEN_ID_TENANT_SPECIFIC_FIELDS" "SPIFFWORKFLOW_BACKEND_OPEN_ID_TENANT_SPECIFIC_FIELDS"
) )
SPIFFWORKFLOW_BACKEND_AUTHENTICATION_DISABLED = (
environ.get("SPIFFWORKFLOW_BACKEND_AUTHENTICATION_DISABLED", default="false") == "true"
)
# loggers to use is a comma separated list of logger prefixes that we will be converted to list of strings # loggers to use is a comma separated list of logger prefixes that we will be converted to list of strings
SPIFFWORKFLOW_BACKEND_LOGGERS_TO_USE = environ.get("SPIFFWORKFLOW_BACKEND_LOGGERS_TO_USE") SPIFFWORKFLOW_BACKEND_LOGGERS_TO_USE = environ.get("SPIFFWORKFLOW_BACKEND_LOGGERS_TO_USE")

View File

@ -15,13 +15,14 @@ if TYPE_CHECKING:
from spiffworkflow_backend.models.user import UserModel # noqa: F401 from spiffworkflow_backend.models.user import UserModel # noqa: F401
SPIFF_NO_AUTH_ANONYMOUS_GROUP = "spiff_anonymous_group"
class GroupNotFoundError(Exception): class GroupNotFoundError(Exception):
"""GroupNotFoundError.""" pass
class GroupModel(SpiffworkflowBaseDBModel): class GroupModel(SpiffworkflowBaseDBModel):
"""GroupModel."""
__tablename__ = "group" __tablename__ = "group"
__table_args__ = {"extend_existing": True} __table_args__ = {"extend_existing": True}

View File

@ -32,3 +32,5 @@ class PrincipalModel(SpiffworkflowBaseDBModel):
user = relationship("UserModel", viewonly=True) user = relationship("UserModel", viewonly=True)
group = relationship("GroupModel", viewonly=True) group = relationship("GroupModel", viewonly=True)
permission_assignments = relationship("PermissionAssignmentModel", cascade="delete") # type: ignore

View File

@ -15,14 +15,15 @@ from spiffworkflow_backend.models.db import SpiffworkflowBaseDBModel
from spiffworkflow_backend.models.group import GroupModel from spiffworkflow_backend.models.group import GroupModel
SPIFF_NO_AUTH_ANONYMOUS_USER = "spiff_anonymous_user"
class UserNotFoundError(Exception): class UserNotFoundError(Exception):
"""UserNotFoundError.""" pass
@dataclass @dataclass
class UserModel(SpiffworkflowBaseDBModel): class UserModel(SpiffworkflowBaseDBModel):
"""UserModel."""
__tablename__ = "user" __tablename__ = "user"
__table_args__ = (db.UniqueConstraint("service", "service_id", name="service_key"),) __table_args__ = (db.UniqueConstraint("service", "service_id", name="service_key"),)
@ -47,9 +48,9 @@ class UserModel(SpiffworkflowBaseDBModel):
secondary="user_group_assignment", secondary="user_group_assignment",
overlaps="user_group_assignments,users", overlaps="user_group_assignments,users",
) )
principal = relationship("PrincipalModel", uselist=False) # type: ignore principal = relationship("PrincipalModel", uselist=False, cascade="delete") # type: ignore
def encode_auth_token(self) -> str: def encode_auth_token(self, extra_payload: dict | None = None) -> str:
"""Generate the Auth Token. """Generate the Auth Token.
:return: string :return: string
@ -59,12 +60,16 @@ class UserModel(SpiffworkflowBaseDBModel):
raise KeyError("we need current_app.config to have a SECRET_KEY") raise KeyError("we need current_app.config to have a SECRET_KEY")
# hours = float(app.config['TOKEN_AUTH_TTL_HOURS']) # hours = float(app.config['TOKEN_AUTH_TTL_HOURS'])
payload = { base_payload = {
# 'exp': datetime.datetime.utcnow() + datetime.timedelta(hours=hours, minutes=0, seconds=0), "email": self.email,
# 'iat': datetime.datetime.utcnow(), "preferred_username": self.username,
"sub": f"service:{self.service}::service_id:{self.service_id}", "sub": f"service:{self.service}::service_id:{self.service_id}",
"token_type": "internal", "token_type": "internal",
} }
payload = base_payload
if extra_payload is not None:
payload = {**base_payload, **extra_payload}
return jwt.encode( return jwt.encode(
payload, payload,
secret_key, secret_key,

View File

@ -6,7 +6,6 @@ import re
from typing import Any from typing import Any
from typing import Dict from typing import Dict
from typing import Optional from typing import Optional
from typing import Union
import flask import flask
import jwt import jwt
@ -20,6 +19,10 @@ from werkzeug.wrappers import Response
from spiffworkflow_backend.exceptions.api_error import ApiError from spiffworkflow_backend.exceptions.api_error import ApiError
from spiffworkflow_backend.helpers.api_version import V1_API_PATH_PREFIX from spiffworkflow_backend.helpers.api_version import V1_API_PATH_PREFIX
from spiffworkflow_backend.models.db import db
from spiffworkflow_backend.models.group import GroupModel
from spiffworkflow_backend.models.group import SPIFF_NO_AUTH_ANONYMOUS_GROUP
from spiffworkflow_backend.models.user import SPIFF_NO_AUTH_ANONYMOUS_USER
from spiffworkflow_backend.models.user import UserModel from spiffworkflow_backend.models.user import UserModel
from spiffworkflow_backend.services.authentication_service import AuthenticationService from spiffworkflow_backend.services.authentication_service import AuthenticationService
from spiffworkflow_backend.services.authentication_service import ( from spiffworkflow_backend.services.authentication_service import (
@ -27,6 +30,7 @@ from spiffworkflow_backend.services.authentication_service import (
) )
from spiffworkflow_backend.services.authentication_service import TokenExpiredError from spiffworkflow_backend.services.authentication_service import TokenExpiredError
from spiffworkflow_backend.services.authorization_service import AuthorizationService from spiffworkflow_backend.services.authorization_service import AuthorizationService
from spiffworkflow_backend.services.group_service import GroupService
from spiffworkflow_backend.services.user_service import UserService from spiffworkflow_backend.services.user_service import UserService
""" """
@ -36,9 +40,7 @@ from spiffworkflow_backend.services.user_service import UserService
# authorization_exclusion_list = ['status'] # authorization_exclusion_list = ['status']
def verify_token( def verify_token(token: Optional[str] = None, force_run: Optional[bool] = False) -> None:
token: Optional[str] = None, force_run: Optional[bool] = False
) -> Optional[Dict[str, Optional[Union[str, int]]]]:
"""Verify the token for the user (if provided). """Verify the token for the user (if provided).
If in production environment and token is not provided, gets user from the SSO headers and returns their token. If in production environment and token is not provided, gets user from the SSO headers and returns their token.
@ -82,6 +84,22 @@ def verify_token(
current_app.logger.error( current_app.logger.error(
f"Exception in verify_token getting user from decoded internal token. {e}" f"Exception in verify_token getting user from decoded internal token. {e}"
) )
# if the user is the anonymous user and we have auth enabled then make sure we clean up the anonymouse user
if (
user_model
and not current_app.config.get("SPIFFWORKFLOW_BACKEND_AUTHENTICATION_DISABLED")
and user_model.username == SPIFF_NO_AUTH_ANONYMOUS_USER
and user_model.service_id == "spiff_anonymous_service_id"
):
group_model = GroupModel.query.filter_by(identifier=SPIFF_NO_AUTH_ANONYMOUS_GROUP).first()
db.session.delete(group_model)
db.session.delete(user_model)
db.session.commit()
tld = current_app.config["THREAD_LOCAL_DATA"]
tld.user_has_logged_out = True
return None
elif "iss" in decoded_token.keys(): elif "iss" in decoded_token.keys():
user_info = None user_info = None
try: try:
@ -196,29 +214,22 @@ def set_new_access_token_in_cookie(
return response return response
def encode_auth_token(sub: str, token_type: Optional[str] = None) -> str:
"""Generates the Auth Token.
:return: string
"""
payload = {"sub": sub}
if token_type is None:
token_type = "internal" # noqa: S105
payload["token_type"] = token_type
if "SECRET_KEY" in current_app.config:
secret_key = current_app.config.get("SECRET_KEY")
else:
current_app.logger.error("Missing SECRET_KEY in encode_auth_token")
raise ApiError(error_code="encode_error", message="Missing SECRET_KEY in encode_auth_token")
return jwt.encode(
payload,
str(secret_key),
algorithm="HS256",
)
def login(redirect_url: str = "/") -> Response: def login(redirect_url: str = "/") -> Response:
"""Login.""" if current_app.config.get("SPIFFWORKFLOW_BACKEND_AUTHENTICATION_DISABLED"):
user = UserModel.query.filter_by(username=SPIFF_NO_AUTH_ANONYMOUS_USER).first()
if user is None:
user = UserService.create_user(
SPIFF_NO_AUTH_ANONYMOUS_USER, "spiff_anonymous_service", "spiff_anonymous_service_id"
)
GroupService.add_user_to_group_or_add_to_waiting(user.username, SPIFF_NO_AUTH_ANONYMOUS_GROUP)
AuthorizationService.add_permission_from_uri_or_macro(SPIFF_NO_AUTH_ANONYMOUS_GROUP, "all", "/*")
g.user = user
g.token = user.encode_auth_token({"authentication_disabled": True})
tld = current_app.config["THREAD_LOCAL_DATA"]
tld.new_access_token = g.token
tld.new_id_token = g.token
return redirect(redirect_url)
state = AuthenticationService.generate_state(redirect_url) state = AuthenticationService.generate_state(redirect_url)
login_redirect_url = AuthenticationService().get_login_redirect_url(state.decode("UTF-8")) login_redirect_url = AuthenticationService().get_login_redirect_url(state.decode("UTF-8"))
return redirect(login_redirect_url) return redirect(login_redirect_url)

View File

@ -33,7 +33,6 @@ class UserService:
tenant_specific_field_2: Optional[str] = None, tenant_specific_field_2: Optional[str] = None,
tenant_specific_field_3: Optional[str] = None, tenant_specific_field_3: Optional[str] = None,
) -> UserModel: ) -> UserModel:
"""Create_user."""
user_model: Optional[UserModel] = ( user_model: Optional[UserModel] = (
UserModel.query.filter(UserModel.service == service).filter(UserModel.service_id == service_id).first() UserModel.query.filter(UserModel.service == service).filter(UserModel.service_id == service_id).first()
) )

View File

@ -120,15 +120,19 @@ export default function NavigationBar() {
<a target="_blank" href={documentationUrl} rel="noreferrer"> <a target="_blank" href={documentationUrl} rel="noreferrer">
Documentation Documentation
</a> </a>
<hr /> {!UserService.authenticationDisabled() ? (
<Button <>
data-qa="logout-button" <hr />
className="button-link" <Button
onClick={handleLogout} data-qa="logout-button"
> className="button-link"
<Logout /> onClick={handleLogout}
&nbsp;&nbsp;Sign out >
</Button> <Logout />
&nbsp;&nbsp;Sign out
</Button>
</>
) : null}
</ToggletipContent> </ToggletipContent>
</Toggletip> </Toggletip>
</div> </div>

View File

@ -62,6 +62,15 @@ const getUserEmail = () => {
return null; return null;
}; };
const authenticationDisabled = () => {
const idToken = getIdToken();
if (idToken) {
const idObject = jwt(idToken);
return (idObject as any).authentication_disabled;
}
return false;
};
const getPreferredUsername = () => { const getPreferredUsername = () => {
const idToken = getIdToken(); const idToken = getIdToken();
if (idToken) { if (idToken) {
@ -82,14 +91,15 @@ const hasRole = (_roles: string[]): boolean => {
}; };
const UserService = { const UserService = {
authenticationDisabled,
doLogin, doLogin,
doLogout, doLogout,
isLoggedIn,
getAccessToken, getAccessToken,
loginIfNeeded,
getPreferredUsername, getPreferredUsername,
getUserEmail, getUserEmail,
hasRole, hasRole,
isLoggedIn,
loginIfNeeded,
}; };
export default UserService; export default UserService;