Skip to content

Commit

Permalink
Merge branch 'release-0.2.0' into fix/daniel-fix-compliance-report-st…
Browse files Browse the repository at this point in the history
…atus-1540
  • Loading branch information
prv-proton authored Jan 4, 2025
2 parents 88a7536 + 6049bb1 commit 00e0982
Show file tree
Hide file tree
Showing 9 changed files with 188 additions and 97 deletions.
20 changes: 9 additions & 11 deletions backend/lcfs/web/api/organizations/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -162,25 +162,23 @@ async def get_organization_types(
return await service.get_organization_types()


# TODO review security of this endpoint around returning balances
# for all organizations
@router.get(
"/names/",
response_model=List[OrganizationSummaryResponseSchema],
status_code=status.HTTP_200_OK,
)
@cache(expire=1) # cache for 1 hour
@view_handler(["*"])
@cache(expire=1) # Cache for 1 hour
@view_handler(
[RoleEnum.GOVERNMENT]
) # Ensure only government can access this endpoint because it returns balances
async def get_organization_names(
request: Request, service: OrganizationsService = Depends()
request: Request,
only_registered: bool = Query(True),
service: OrganizationsService = Depends(),
):
"""Fetch all organization names"""

# Set the default sorting order
"""Fetch all organization names."""
order_by = ("name", "asc")

# Call the service with only_registered set to True to fetch only registered organizations
return await service.get_organization_names(True, order_by)
return await service.get_organization_names(only_registered, order_by)


@router.get(
Expand Down
179 changes: 126 additions & 53 deletions etl/nifi_scripts/user.groovy
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import java.sql.Connection
import java.sql.PreparedStatement
import java.sql.ResultSet
import groovy.json.JsonSlurper

log.warn("**** STARTING USER ETL ****")
log.warn('**** STARTING USER ETL ****')

// SQL query to extract user profiles
def userProfileQuery = """
Expand All @@ -17,33 +18,98 @@ def userProfileQuery = """
first_name,
last_name,
is_active,
CASE WHEN organization_id = 1 THEN null ELSE organization_id END as organization_id
CASE WHEN organization_id = 1 THEN NULL ELSE organization_id END as organization_id
FROM public.user;
"""

// SQL query to extract user roles
def userRoleQuery = """
SELECT ur.user_id as user_profile_id,
CASE
WHEN r.name = 'Admin' THEN 'ADMINISTRATOR'
WHEN r.name = 'GovUser' THEN 'ANALYST'
WHEN r.name IN ('GovDirector', 'GovDeputyDirector') THEN 'DIRECTOR'
WHEN r.name = 'GovComplianceManager' THEN 'COMPLIANCE_MANAGER'
WHEN r.name = 'FSAdmin' THEN 'MANAGE_USERS'
WHEN r.name = 'FSUser' THEN 'TRANSFER'
WHEN r.name = 'FSManager' THEN 'SIGNING_AUTHORITY'
WHEN r.name = 'FSNoAccess' THEN 'READ_ONLY'
WHEN r.name = 'ComplianceReporting' THEN 'COMPLIANCE_REPORTING'
ELSE NULL
END AS role_name
FROM public.user u
INNER JOIN user_role ur ON ur.user_id = u.id
INNER JOIN role r ON r.id = ur.role_id
WHERE r.name NOT IN ('FSDocSubmit', 'GovDoc');
WITH RoleData AS (
SELECT ur.user_id AS user_profile_id,
CASE
-- Government Roles
WHEN u.organization_id = 1 THEN
CASE
WHEN r.name = 'Admin' THEN 'ADMINISTRATOR'
WHEN r.name = 'GovUser' THEN 'ANALYST'
WHEN r.name IN ('GovDirector', 'GovDeputyDirector') THEN 'DIRECTOR'
WHEN r.name = 'GovComplianceManager' THEN 'COMPLIANCE_MANAGER'
END
-- Supplier Roles
WHEN u.organization_id > 1 THEN
CASE
WHEN r.name = 'FSAdmin' THEN 'MANAGE_USERS'
WHEN r.name = 'FSUser' THEN 'TRANSFER'
WHEN r.name = 'FSManager' THEN 'SIGNING_AUTHORITY'
WHEN r.name = 'FSNoAccess' THEN 'READ_ONLY'
WHEN r.name = 'ComplianceReporting' THEN 'COMPLIANCE_REPORTING'
END
END AS role_name,
u.organization_id
FROM public.user u
INNER JOIN user_role ur ON ur.user_id = u.id
INNER JOIN role r ON r.id = ur.role_id
WHERE r.name NOT IN ('FSDocSubmit', 'GovDoc')
),
FilteredRoles AS (
SELECT user_profile_id,
organization_id,
ARRAY_AGG(role_name) AS roles
FROM RoleData
WHERE role_name IS NOT NULL
GROUP BY user_profile_id, organization_id
),
ProcessedRoles AS (
SELECT
user_profile_id,
CASE
-- Rule 1: Government Users
WHEN organization_id = 1 THEN
CASE
-- Retain Administrator and one prioritized gov role
WHEN 'ADMINISTRATOR' = ANY(roles) THEN
ARRAY_REMOVE(ARRAY[
'ADMINISTRATOR',
CASE
WHEN 'DIRECTOR' = ANY(roles) THEN 'DIRECTOR'
WHEN 'COMPLIANCE_MANAGER' = ANY(roles) THEN 'COMPLIANCE_MANAGER'
WHEN 'ANALYST' = ANY(roles) THEN 'ANALYST'
END
], NULL)
-- Priority among gov roles (no Administrator)
ELSE ARRAY_REMOVE(ARRAY[
CASE
WHEN 'DIRECTOR' = ANY(roles) THEN 'DIRECTOR'
WHEN 'COMPLIANCE_MANAGER' = ANY(roles) THEN 'COMPLIANCE_MANAGER'
WHEN 'ANALYST' = ANY(roles) THEN 'ANALYST'
END
], NULL)
END
-- Rule 2: Supplier Users
WHEN organization_id > 1 THEN
CASE
-- Return empty array if READ_ONLY exists
WHEN 'READ_ONLY' = ANY(roles) THEN
ARRAY[]::text[]
ELSE ARRAY(
SELECT UNNEST(roles)
EXCEPT
SELECT UNNEST(ARRAY['ADMINISTRATOR', 'ANALYST', 'DIRECTOR', 'COMPLIANCE_MANAGER'])
)
END
END AS filtered_roles,
organization_id
FROM FilteredRoles
)
SELECT
user_profile_id,
organization_id,
array_to_string(filtered_roles, ',') as roles_string
FROM ProcessedRoles;
"""

// SQL queries to insert user profiles and roles into destination tables with ON CONFLICT handling
def insertUserProfileSQL = """
def insertUserProfileSQL = '''
INSERT INTO user_profile (user_profile_id, keycloak_user_id, keycloak_email, keycloak_username, email, title, phone, mobile_phone, first_name, last_name, is_active, organization_id)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
ON CONFLICT (user_profile_id) DO UPDATE
Expand All @@ -58,17 +124,17 @@ def insertUserProfileSQL = """
last_name = EXCLUDED.last_name,
is_active = EXCLUDED.is_active,
organization_id = EXCLUDED.organization_id;
"""
'''

def insertUserRoleSQL = """
def insertUserRoleSQL = '''
INSERT INTO user_role (user_profile_id, role_id)
VALUES (?, (SELECT role_id FROM role WHERE name = ?::role_enum))
ON CONFLICT (user_profile_id, role_id) DO NOTHING;
"""
'''

// Fetch connections to both source and destination databases
def sourceDbcpService = context.controllerServiceLookup.getControllerService("3245b078-0192-1000-ffff-ffffba20c1eb")
def destinationDbcpService = context.controllerServiceLookup.getControllerService("3244bf63-0192-1000-ffff-ffffc8ec6d93")
def sourceDbcpService = context.controllerServiceLookup.getControllerService('3245b078-0192-1000-ffff-ffffba20c1eb')
def destinationDbcpService = context.controllerServiceLookup.getControllerService('3244bf63-0192-1000-ffff-ffffc8ec6d93')

Connection sourceConn = null
Connection destinationConn = null
Expand All @@ -88,18 +154,18 @@ try {

// Process the result set for user profiles
while (userProfileResultSet.next()) {
def userProfileId = userProfileResultSet.getInt("user_profile_id")
def keycloakUserId = userProfileResultSet.getString("keycloak_user_id")
def keycloakEmail = userProfileResultSet.getString("keycloak_email")
def keycloakUsername = userProfileResultSet.getString("keycloak_username")
def email = userProfileResultSet.getString("email")
def title = userProfileResultSet.getString("title")
def phone = userProfileResultSet.getString("phone")
def mobilePhone = userProfileResultSet.getString("mobile_phone")
def firstName = userProfileResultSet.getString("first_name")
def lastName = userProfileResultSet.getString("last_name")
def isActive = userProfileResultSet.getBoolean("is_active")
def organizationId = userProfileResultSet.getObject("organization_id") // Nullable
def userProfileId = userProfileResultSet.getInt('user_profile_id')
def keycloakUserId = userProfileResultSet.getString('keycloak_user_id')
def keycloakEmail = userProfileResultSet.getString('keycloak_email')
def keycloakUsername = userProfileResultSet.getString('keycloak_username')
def email = userProfileResultSet.getString('email')
def title = userProfileResultSet.getString('title')
def phone = userProfileResultSet.getString('phone')
def mobilePhone = userProfileResultSet.getString('mobile_phone')
def firstName = userProfileResultSet.getString('first_name')
def lastName = userProfileResultSet.getString('last_name')
def isActive = userProfileResultSet.getBoolean('is_active')
def organizationId = userProfileResultSet.getObject('organization_id') // Nullable

// Bind values to the prepared statement
insertUserProfileStmt.setInt(1, userProfileId)
Expand Down Expand Up @@ -136,25 +202,32 @@ try {
PreparedStatement sourceRoleStmt = sourceConn.prepareStatement(userRoleQuery)
ResultSet userRoleResultSet = sourceRoleStmt.executeQuery()

// Process the result set for user roles
while (userRoleResultSet.next()) {
def userProfileId = userRoleResultSet.getInt("user_profile_id")
def roleName = userRoleResultSet.getString("role_name")

// Bind values to the prepared statement
insertUserRoleStmt.setInt(1, userProfileId)
insertUserRoleStmt.setString(2, roleName)

// Execute the insert/update for user roles
insertUserRoleStmt.executeUpdate()
def userProfileId = userRoleResultSet.getInt('user_profile_id')
def rolesString = userRoleResultSet.getString('roles_string')

if (rolesString) {
def roles = rolesString.split(',')

roles.each { role ->
try {
insertUserRoleStmt.setInt(1, userProfileId)
insertUserRoleStmt.setString(2, role)
insertUserRoleStmt.executeUpdate()
log.info("Successfully inserted role ${role} for user ${userProfileId}")
} catch (Exception e) {
log.error("Failed to insert role ${role} for user ${userProfileId}: ${e.message}")
}
}
} else {
log.warn("No roles found for user ${userProfileId}")
}
}

} catch (Exception e) {
log.error("Error occurred while processing data", e)
log.error('Error occurred during ETL process', e)
} finally {
// Close the connections
if (sourceConn != null) sourceConn.close()
if (destinationConn != null) destinationConn.close()
if (sourceConn) sourceConn.close()
if (destinationConn) destinationConn.close()
}

log.warn("**** COMPLETED USER ETL ****")
log.warn('**** COMPLETED USER ETL ****')
11 changes: 7 additions & 4 deletions frontend/src/hooks/useOrganizations.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,16 @@ export const useOrganizationStatuses = (options) => {
})
}

export const useOrganizationNames = (options) => {
export const useOrganizationNames = (onlyRegistered = true, options) => {
const client = useApiService()

return useQuery({
queryKey: ['organization-names'],
queryFn: async () => (await client.get('/organizations/names/')).data,
...options
queryKey: ['organization-names', onlyRegistered],
queryFn: async () => {
const response = await client.get(`/organizations/names/?only_registered=${onlyRegistered}`)
return response.data
},
...options,
})
}

Expand Down
29 changes: 29 additions & 0 deletions frontend/src/utils/grid/cellRenderers.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,35 @@ export const StatusRenderer = (props) => {
)
}

export const LoginStatusRenderer = (props) => {
return (
<BCBox
component={props.isView ? 'span' : 'div'}
mt={1}
sx={{ width: '100%', height: '100%' }}
>
<BCBadge
badgeContent={props.data.isLoginSuccessful ? 'True' : 'False'}
color={props.data.isLoginSuccessful ? 'success' : 'error'}
variant="gradient"
size="md"
sx={{
...(!props.isView
? { display: 'flex', justifyContent: 'center' }
: {}),
'& .MuiBadge-badge': {
minWidth: '120px',
fontWeight: 'regular',
textTransform: 'capitalize',
fontSize: '0.875rem',
padding: '0.4em 0.6em'
}
}}
/>
</BCBox>
)
}

export const OrgStatusRenderer = (props) => {
const location = useLocation()
const statusArr = getAllOrganizationStatuses()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ export const UserLoginHistory = () => {
const { t } = useTranslation(['common', 'admin'])

const getRowId = useCallback((params) => {
return params.data.userLoginHistoryId
return params.data.userLoginHistoryId.toString()
}, [])

return (
Expand Down
24 changes: 6 additions & 18 deletions frontend/src/views/Admin/AdminMenu/components/_schema.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
} from '@/utils/formatters'
import {
LinkRenderer,
LoginStatusRenderer,
RoleRenderer,
StatusRenderer
} from '@/utils/grid/cellRenderers'
Expand Down Expand Up @@ -113,15 +114,6 @@ export const usersColumnDefs = (t) => [
}
]

export const usersDefaultColDef = {
resizable: true,
sortable: true,
filter: true,
minWidth: 300,
floatingFilter: true, // enables the filter boxes under the header label
suppressHeaderMenuButton: true // suppresses the menu button appearing next to the Header Label
}

export const idirUserDefaultFilter = [
{ filterType: 'text', type: 'blank', field: 'organizationId', filter: '' }
]
Expand Down Expand Up @@ -175,28 +167,24 @@ export const userLoginHistoryColDefs = (t) => [
},
{
field: 'keycloakEmail',
headerName: t('admin:userLoginHistoryColLabels.keycloakEmail'),
cellDataType: 'string'
headerName: t('admin:userLoginHistoryColLabels.keycloakEmail')
},
{
field: 'keycloakUserId',
headerName: t('admin:userLoginHistoryColLabels.keycloakUserId'),
cellDataType: 'string'
headerName: t('admin:userLoginHistoryColLabels.keycloakUserId')
},
{
field: 'externalUsername',
headerName: t('admin:userLoginHistoryColLabels.externalUsername'),
cellDataType: 'string'
headerName: t('admin:userLoginHistoryColLabels.externalUsername')
},
{
field: 'isLoginSuccessful',
headerName: t('admin:userLoginHistoryColLabels.isLoginSuccessful'),
cellDataType: 'boolean'
cellRenderer: LoginStatusRenderer
},
{
field: 'loginErrorMessage',
headerName: t('admin:userLoginHistoryColLabels.loginErrorMessage'),
cellDataType: 'string'
headerName: t('admin:userLoginHistoryColLabels.loginErrorMessage')
},
{
field: 'createDate',
Expand Down
Loading

0 comments on commit 00e0982

Please sign in to comment.