Skip to content

Commit

Permalink
Allow DA admins to view share logs (data-dot-all#1274)
Browse files Browse the repository at this point in the history
### Feature or Bugfix
<!-- please choose -->
- Feature

### Detail
- The latest logstream (last 24 hours) is loaded into modal window on
Share Object page
- User can see "Logs" button, if they are admins, stewards or owner of
the dataset


### Relates
- data-dot-all#1215 

### Security
Please answer the questions below briefly where applicable, or write
`N/A`. Based on
[OWASP 10](https://owasp.org/Top10/en/).

- Does this PR introduce or modify any input fields or queries - this
includes
fetching data from storage outside the application (e.g. a database, an
S3 bucket)?
  - Is the input sanitized?
- What precautions are you taking before deserializing the data you
consume?
  - Is injection prevented by parametrizing queries?
  - Have you ensured no `eval` or similar functions are used?
- Does this PR introduce any functionality or component that requires
authorization?
- How have you ensured it respects the existing AuthN/AuthZ mechanisms?
  - Are you logging failed auth attempts?
- Are you using or adding any cryptographic features?
  - Do you use a standard proven implementations?
  - Are the used keys controlled by the customer? Where are they stored?
- Are you introducing any new policies/roles/users?
  - Have you used the least-privilege principle? How?


By submitting this pull request, I confirm that my contribution is made
under the terms of the Apache 2.0 license.

---------

Co-authored-by: Sofia Sazonova <[email protected]>
  • Loading branch information
SofiaSazonova and Sofia Sazonova authored May 23, 2024
1 parent a0fc4b0 commit 6c59e9d
Show file tree
Hide file tree
Showing 9 changed files with 264 additions and 0 deletions.
9 changes: 9 additions & 0 deletions backend/dataall/modules/s3_datasets_shares/api/queries.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
from dataall.modules.s3_datasets_shares.api.resolvers import (
get_dataset_shared_assume_role_url,
get_share_object,
get_share_logs,
list_shared_with_environment_data_items,
list_shares_in_my_inbox,
list_shares_in_my_outbox,
Expand Down Expand Up @@ -70,3 +71,11 @@
resolver=get_dataset_shared_assume_role_url,
test_scope='Dataset',
)


getShareLogs = gql.QueryField(
name='getShareLogs',
args=[gql.Argument(name='shareUri', type=gql.NonNullableType(gql.String))],
type=gql.ArrayType(gql.Ref('ShareLog')),
resolver=get_share_logs,
)
9 changes: 9 additions & 0 deletions backend/dataall/modules/s3_datasets_shares/api/resolvers.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
from dataall.modules.s3_datasets.db.dataset_repositories import DatasetRepository
from dataall.modules.s3_datasets.db.dataset_models import DatasetStorageLocation, DatasetTable, S3Dataset


log = logging.getLogger(__name__)


Expand Down Expand Up @@ -132,6 +133,10 @@ def get_share_object(context, source, shareUri: str = None):
return ShareObjectService.get_share_object(uri=shareUri)


def get_share_logs(context, source, shareUri: str):
return ShareObjectService.get_share_logs(shareUri)


def resolve_user_role(context: Context, source: ShareObject, **kwargs):
if not source:
return None
Expand Down Expand Up @@ -164,6 +169,10 @@ def resolve_user_role(context: Context, source: ShareObject, **kwargs):
)


def resolve_can_view_logs(context: Context, source: ShareObject):
return ShareObjectService.check_view_log_permissions(context.username, context.groups, source.shareUri)


def resolve_dataset(context: Context, source: ShareObject, **kwargs):
if not source:
return None
Expand Down
16 changes: 16 additions & 0 deletions backend/dataall/modules/s3_datasets_shares/api/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
list_shareable_objects,
resolve_user_role,
resolve_shared_database_name,
resolve_can_view_logs,
)
from dataall.core.environment.api.resolvers import resolve_environment

Expand Down Expand Up @@ -166,6 +167,11 @@
type=gql.Ref('SharedItemSearchResult'),
resolver=list_shareable_objects,
),
gql.Field(
name='canViewLogs',
resolver=resolve_can_view_logs,
type=gql.Boolean,
),
gql.Field(
name='userRoleForShareObject',
type=gql.Ref('ShareObjectPermission'),
Expand Down Expand Up @@ -261,3 +267,13 @@
gql.Field(name='hasPrevious', type=gql.Boolean),
],
)

ShareLog = gql.ObjectType(
name='ShareLog',
fields=[
gql.Field(name='logStream', type=gql.String),
gql.Field(name='logGroup', type=gql.String),
gql.Field(name='timestamp', type=gql.String),
gql.Field(name='message', type=gql.String),
],
)
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import os
from datetime import datetime
from warnings import warn

Expand Down Expand Up @@ -45,12 +46,24 @@
from dataall.modules.s3_datasets.services.dataset_permissions import DATASET_TABLE_READ, DATASET_FOLDER_READ
from dataall.base.aws.iam import IAM

from dataall.base.utils import Parameter
from dataall.base.db import exceptions
from dataall.core.stacks.aws.cloudwatch import CloudWatch


import logging

log = logging.getLogger(__name__)


class ShareObjectService:
@staticmethod
def check_view_log_permissions(username, groups, shareUri):
with get_context().db_engine.scoped_session() as session:
share = ShareObjectRepository.get_share_by_uri(session, shareUri)
ds = DatasetRepository.get_dataset_by_uri(session, share.datasetUri)
return ds.stewards in groups or ds.SamlAdminGroupName in groups or username == ds.owner

@staticmethod
def verify_principal_role(session, share: ShareObject) -> bool:
role_name = share.principalIAMRoleName
Expand Down Expand Up @@ -590,3 +603,55 @@ def attach_dataset_folder_read_permission(session, share):
log.info(
f'Resource permission policy {DATASET_FOLDER_READ} to table {location.itemUri} for group {share.groupUri} already exists. Skip... '
)

@staticmethod
def get_share_logs_name_query(shareUri):
log.info(f'Get share Logs stream name for share {shareUri}')

query = f"""fields @logStream
|filter @message like '{shareUri}'
| sort @timestamp desc
| limit 1
"""
return query

@staticmethod
def get_share_logs_query(log_stream_name):
query = f"""fields @timestamp, @message, @logStream, @log as @logGroup
| sort @timestamp asc
| filter @logStream like "{log_stream_name}"
"""
return query

@staticmethod
def get_share_logs(shareUri):
context = get_context()
if not ShareObjectService.check_view_log_permissions(context.username, context.groups, shareUri):
raise exceptions.ResourceUnauthorized(
username=context.username,
action='View Share Logs',
resource_uri=shareUri,
)

envname = os.getenv('envname', 'local')
log_group_name = f"/{Parameter().get_parameter(env=envname, path='resourcePrefix')}/{envname}/ecs/share-manager"

query_for_name = ShareObjectService.get_share_logs_name_query(shareUri=shareUri)
name_query_result = CloudWatch.run_query(
query=query_for_name,
log_group_name=log_group_name,
days=1,
)
if len(name_query_result) == 0:
return []

name = name_query_result[0]['logStream']

query = ShareObjectService.get_share_logs_query(log_stream_name=name)
results = CloudWatch.run_query(
query=query,
log_group_name=log_group_name,
days=1,
)
log.info(f'Running Logs query {query} for log_group_name={log_group_name}')
return results
119 changes: 119 additions & 0 deletions frontend/src/modules/Shares/components/ShareLogs.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
import Editor from '@monaco-editor/react';
import { RefreshRounded } from '@mui/icons-material';
import {
Box,
Button,
CircularProgress,
Dialog,
Grid,
Typography
} from '@mui/material';
import PropTypes from 'prop-types';
import React, { useCallback, useEffect, useState } from 'react';
import { THEMES, useSettings } from 'design';
import { SET_ERROR, useDispatch } from 'globalErrors';
import { getShareLogs, useClient } from 'services';

export const ShareLogs = (props) => {
const { shareUri, onClose, open } = props;
const { settings } = useSettings();
const client = useClient();
const dispatch = useDispatch();
const [logs, setLogs] = useState(null);
const [loading, setLoading] = useState(true);

const getLogs = useCallback(async () => {
setLoading(true);
try {
const response = await client.query(getShareLogs(shareUri));
if (response && !response.errors) {
setLogs(response.data.getShareLogs.map((l) => l.message));
} else {
dispatch({ type: SET_ERROR, error: response.errors[0].message });
}
} catch (e) {
dispatch({ type: SET_ERROR, error: e.message });
}
setLoading(false);
}, [client, dispatch, shareUri]);

useEffect(() => {
if (client && open) {
getLogs().catch((e) => dispatch({ type: SET_ERROR, error: e.message }));
}
}, [client, dispatch, getLogs, open]);

return (
<Dialog maxWidth="md" fullWidth onClose={onClose} open={open}>
<Box sx={{ p: 3 }}>
<Grid container justifyContent="space-between" spacing={3}>
<Grid item>
<Typography color="textPrimary" gutterBottom variant="h6">
Logs for share {shareUri}
</Typography>
</Grid>
<Grid item />
<Box sx={{ m: -1, mt: 2 }}>
<Button
color="primary"
startIcon={<RefreshRounded fontSize="small" />}
variant="outlined"
onClick={getLogs}
>
Refresh
</Button>
</Box>
</Grid>
<Box sx={{ height: '35rem' }}>
{loading ? (
<CircularProgress />
) : (
<Box>
{logs && (
<Box sx={{ mt: 1 }}>
<div
style={{
width: '100%',
border:
settings.theme === THEMES.LIGHT ? '1px solid #eee' : ''
}}
>
<Editor
value={
logs.length > 0
? logs.join('\n')
: 'No logs available for the last 24 hours. Logs may take few minutes after the share is processed...'
}
options={{ minimap: { enabled: false } }}
theme="vs-dark"
inDiffEditor={false}
height="35rem"
language="text"
showPrintMargin
showGutter
highlightActiveLine
editorProps={{
$blockScrolling: Infinity
}}
setOptions={{
enableBasicAutocompletion: true,
enableLiveAutocompletion: true,
enableSnippets: true,
showLineNumbers: true,
tabSize: 2
}}
/>
</div>
</Box>
)}
</Box>
)}
</Box>
</Box>
</Dialog>
);
};

ShareLogs.propTypes = {
shareUri: PropTypes.string.isRequired
};
1 change: 1 addition & 0 deletions frontend/src/modules/Shares/services/getShareObject.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export const getShareObject = ({ shareUri, filter }) => ({
requestPurpose
rejectPurpose
userRoleForShareObject
canViewLogs
consumptionData {
s3AccessPointName
sharedGlueDatabase
Expand Down
29 changes: 29 additions & 0 deletions frontend/src/modules/Shares/views/ShareView.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import {
Article,
BlockOutlined,
CheckCircleOutlined,
CopyAllOutlined,
Expand Down Expand Up @@ -73,6 +74,7 @@ import {
UpdateRequestReason
} from '../components';
import { generateShareItemLabel } from 'utils';
import { ShareLogs } from '../components/ShareLogs';

function ShareViewHeader(props) {
const {
Expand All @@ -89,6 +91,8 @@ function ShareViewHeader(props) {
const [rejecting, setRejecting] = useState(false);
const [submitting, setSubmitting] = useState(false);
const [isRejectShareModalOpen, setIsRejectShareModalOpen] = useState(false);
const [openLogsModal, setOpenLogsModal] = useState(null);

const submit = async () => {
setSubmitting(true);
const response = await client.mutate(
Expand Down Expand Up @@ -133,6 +137,13 @@ function ShareViewHeader(props) {
}
};

const handleOpenLogsModal = () => {
setOpenLogsModal(true);
};
const handleCloseOpenLogs = () => {
setOpenLogsModal(false);
};

const handleRejectShareModalOpen = () => {
setIsRejectShareModalOpen(true);
};
Expand Down Expand Up @@ -246,6 +257,17 @@ function ShareViewHeader(props) {
>
Refresh
</Button>
{share.canViewLogs && (
<Button
color="primary"
startIcon={<Article fontSize="small" />}
sx={{ m: 1 }}
variant="outlined"
onClick={handleOpenLogsModal}
>
Logs
</Button>
)}
{(share.userRoleForShareObject === 'Approvers' ||
share.userRoleForShareObject === 'ApproversAndRequesters') && (
<>
Expand Down Expand Up @@ -319,6 +341,13 @@ function ShareViewHeader(props) {
rejectFunction={reject}
/>
)}
{share.canViewLogs && (
<ShareLogs
shareUri={share.shareUri}
onClose={handleCloseOpenLogs}
open={openLogsModal && share.canViewLogs}
/>
)}
</>
);
}
Expand Down
15 changes: 15 additions & 0 deletions frontend/src/services/graphql/ShareObject/getShareLogs.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { gql } from 'apollo-boost';

export const getShareLogs = (shareUri) => ({
variables: {
shareUri
},
query: gql`
query getShareLogs($shareUri: String!) {
getShareLogs(shareUri: $shareUri) {
message
timestamp
}
}
`
});
1 change: 1 addition & 0 deletions frontend/src/services/graphql/ShareObject/index.js
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export * from './createShareObject';
export * from './getShareRequestsToMe';
export * from './getShareLogs';

0 comments on commit 6c59e9d

Please sign in to comment.