Skip to content

Commit 78a900e

Browse files
committed
CMR-10254: Adding AccessControl functionality, tests, code structure, and Docker.
1 parent 8b71d4c commit 78a900e

File tree

13 files changed

+507
-27
lines changed

13 files changed

+507
-27
lines changed

subscription/Dockerfile

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,23 +5,25 @@ ARG QUEUE_URL
55
ARG DEAD_LETTER_QUEUE_URL
66
ARG SNS_NAME
77
ARG SUB_DEAD_LETTER_QUEUE_URL
8+
ARG ENVIRONMENT_NAME
89

910
#Set environment variables
1011
ENV AWS_REGION=$AWS_REGION
1112
ENV QUEUE_URL=$QUEUE_URL
1213
ENV DEAD_LETTER_QUEUE_URL=$DEAD_LETTER_QUEUE_URL
13-
ENV LONG_POLL_TIME 10
14+
ENV LONG_POLL_TIME=10
1415
ENV SNS_NAME=$SNS_NAME
1516
ENV SUB_DEAD_LETTER_QUEUE_URL=$SUB_DEAD_LETTER_QUEUE_URL
17+
ENV ENVIRONMENT_NAME=$ENVIRONMENT_NAME
1618

1719
#Set working directory
1820
WORKDIR /app
1921

2022
#Copy the application files
21-
COPY *.py .
23+
COPY src/*.py .
2224

2325
#Install the required packages
24-
RUN pip3 install boto3 Flask
26+
RUN pip3 install boto3 Flask requests
2527

2628
#EXPOSE 8089
2729
# Command to run the application

subscription/README.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,8 @@ Buid the project: docker build -t {AWS Repository}/cmr-subscription-worker-{env}
1010
Log in docker to the AWS repository: aws ecr get-login-password --region us-east-1 | docker login --username AWS --password-stdin {AWS Repository}
1111
Using docker to push the deployment artifact: docker push {AWS Repository}/cmr-subscription-worker-{env}:latest
1212
For the ECS to update the service: aws ecs update-service --force-new-deployment --service subscription-worker-sit --cluster cmr-service-sit
13+
14+
15+
## locally
16+
docker build -f Dockerfile.local -t subscription_worker .
17+
run script start.sh

subscription/build.sh

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
1-
zip deployment_package.zip subscription_worker.py sns.py part1_docker part_docker
1+
#zip deployment_package.zip subscription_worker.py sns.py part1_docker part_docker
2+
zip deployment_package.zip src Dockerfile

subscription/run-tests-cicd.sh

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
11
#!/bin/bash
22

3-
pip3 install boto3 Flask
4-
python3 -m unittest -v
3+
# This works because I did export PYTHONPATH=src
4+
5+
pip3 install boto3 Flask requests
6+
#python3 -m unittest -v
7+
python3 -m unittest discover -v -s ./test -p "*_test.py"

subscription/src/access_control.py

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
import os
2+
import requests
3+
from env_vars import Env_Vars
4+
from sys import stdout
5+
from logger import logger
6+
7+
class AccessControl:
8+
"""Encapsulates Access Control API.
9+
This class needs the following environment variables set:
10+
For local development:
11+
ACCESS_CONTROL_URL=http://localhost:3011/access-control
12+
13+
For AWS:
14+
ENVIRONMENT_NAME=SIT
15+
CMR_ACCESS_CONTROL_PROTOCOL=https
16+
CMR_ACCESS_CONTROL_PORT=3011
17+
CMR_ACCESS_CONTROL_HOST=cmr.sit.earthdata.nasa.gov
18+
CMR_ACCESS_CONTROL_RELATIVE_ROOT_URL=access-control
19+
20+
Example Use of this class
21+
access_control = AccessControl()
22+
response = access_control.get_permissions('eereiter', 'C1200484253-CMR_ONLY')
23+
The call is the same as 'curl https://cmr.sit.earthdata.nasa.gov/access-control/permissions?user_id=eereiter&concept_id=C1200484253-CMR_ONLY'
24+
Return is either None (Null or Nil) (if check on response is false) or
25+
{"C1200484253-CMR_ONLY":["read","update","delete","order"]}
26+
"""
27+
28+
def __init__(self):
29+
self.url = None
30+
31+
def get_url_from_parameter_store(self):
32+
# Access Control URL is for local development
33+
access_control_url = os.getenv("ACCESS_CONTROL_URL")
34+
35+
if access_control_url:
36+
self.url = access_control_url
37+
return
38+
else:
39+
# This block gets the access_control URL from the AWS parameter store.
40+
environment_name = os.getenv("ENVIRONMENT_NAME")
41+
42+
if not environment_name:
43+
logger.error("ENVIRONMENT_NAME environment variable is not set")
44+
raise ValueError("ENVIRONMENT_NAME environment variable is not set")
45+
46+
# construct the access control parameter names from the environment variable
47+
pre_fix = f"/{environment_name}/ingest/"
48+
protocol_param_name = f"{pre_fix}CMR_ACCESS_CONTROL_PROTOCOL"
49+
port_param_name = f"{pre_fix}CMR_ACCESS_CONTROL_PORT"
50+
host_param_name = f"{pre_fix}CMR_ACCESS_CONTROL_HOST"
51+
context_param_name = f"{pre_fix}CMR_ACCESS_CONTROL_RELATIVE_ROOT_URL"
52+
53+
env_vars = Env_Vars
54+
protocol = env_vars.get_var(protocol_param_name)
55+
port = env_vars.get_var(port_param_name)
56+
host = env_vars.get_var(host_param_name)
57+
context = env_vars.get_var(context_param_name)
58+
self.url = f"{protocol}://{host}:{port}/{context}"
59+
logger.debug("Subscription Worker Access-Control URL:" + self.url)
60+
61+
def get_url(self):
62+
if not self.url:
63+
self.get_url_from_parameter_store()
64+
return self.url
65+
66+
def get_permissions(self, subscriber_id, concept_id):
67+
# Set the access-control permissions URL.
68+
url = f"{self.get_url()}/permissions"
69+
70+
# Set the parameters
71+
params = {
72+
"user_id": subscriber_id,
73+
"concept_id": concept_id
74+
}
75+
76+
# Make a GET request with parameters
77+
response = requests.get(url, params=params)
78+
79+
# Check if the request was successful
80+
if response.status_code == 200:
81+
# Request was successful
82+
data = response.text
83+
logger.debug("Response data:", data)
84+
return data
85+
else:
86+
# Request failed
87+
logger.warning(f"Subscription Worker getting Access Control permissions request using URL {url} with parameters {params} failed with status code: {response.status_code}")

subscription/src/env_vars.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import os
2+
import boto3
3+
from botocore.exceptions import ClientError
4+
from sys import stdout
5+
6+
class Env_Vars:
7+
"""Encapsulates Accessing Variables first from the OS
8+
if not there, then the parameter store."""
9+
10+
def __init__(self):
11+
self.ssm_client = boto3.client('ssm')
12+
13+
def get_var(self, name, decryption=False):
14+
value = os.getenv(name)
15+
if value:
16+
print("Value: " + value)
17+
else:
18+
print("No Value")
19+
20+
21+
if not value:
22+
try:
23+
# Get the parameter value from AWS Parameter Store
24+
response = self.ssm_client.get_parameter(Name=name, WithDecryption=decryption)
25+
value = response['Parameter']['Value']
26+
print("if Value: " + value)
27+
return value
28+
29+
except ClientError as e:
30+
print(f"Error retrieving parameter from AWS Parameter Store: {e}")
31+
stdout.flush()
32+
raise
33+
else:
34+
print("Else Value: " + value)
35+
return value

subscription/src/logger.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import os
2+
import logging
3+
import sys
4+
5+
LOG_LEVEL = os.getenv("LOG_LEVEL")
6+
if not LOG_LEVEL:
7+
LOG_LEVEL = logging.INFO
8+
9+
def setup_logger(name, log_file=None, level=logging.INFO):
10+
"""Function to setup as many loggers as you want"""
11+
12+
formatter = logging.Formatter('%(asctime)s %(levelname)s %(message)s')
13+
14+
handler = logging.StreamHandler(sys.stdout)
15+
handler.setFormatter(formatter)
16+
17+
logger = logging.getLogger(name)
18+
logger.setLevel(level)
19+
logger.addHandler(handler)
20+
21+
if log_file:
22+
file_handler = logging.FileHandler(log_file)
23+
file_handler.setFormatter(formatter)
24+
logger.addHandler(file_handler)
25+
26+
return logger
27+
28+
# Create a default logger
29+
logger = setup_logger(name='default_logger', level=LOG_LEVEL)
Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import boto3
22
import json
3-
from sys import stdout
43
from botocore.exceptions import ClientError
4+
from logger import logger
55

66
class Sns:
77
"""Encapsulates AWS SNS topics."""
@@ -16,8 +16,7 @@ def create_topic(self, topic_name):
1616
try:
1717
topic = self.sns_resource.create_topic(Name=topic_name)
1818
except ClientError as error:
19-
print("Could not get the topic ARN: {error}.")
20-
stdout.flush()
19+
logger.error("Subscription Worker could not get the topic ARN: {error}.")
2120
raise error
2221
else:
2322
return topic
@@ -40,8 +39,7 @@ def publish_message(topic, message):
4039
else:
4140
response = topic.publish(Subject=message_subject, Message=message_message)
4241
except ClientError as error:
43-
print(f"Could not publish message to topic {topic}. {error}")
44-
stdout.flush()
42+
logger.error(f"Subscription Worker could not publish message to topic {topic}. {error}")
4543
raise error
4644
else:
4745
return response
Lines changed: 27 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,15 @@
33
import os
44
from flask import Flask, jsonify
55
from sns import Sns
6-
from sys import stdout
76
from botocore.exceptions import ClientError
7+
from access_control import AccessControl
8+
from logger import logger
89

910
AWS_REGION = os.getenv("AWS_REGION")
1011
QUEUE_URL = os.getenv("QUEUE_URL")
1112
DEAD_LETTER_QUEUE_URL = os.getenv("DEAD_LETTER_QUEUE_URL")
1213
SUB_DEAD_LETTER_QUEUE_URL = os.getenv("SUB_DEAD_LETTER_QUEUE_URL")
13-
LONG_POLL_TIME = os.getenv("LONG_POLL_TIME")
14+
LONG_POLL_TIME = os.getenv("LONG_POLL_TIME", "10")
1415
SNS_NAME = os.getenv("SNS_NAME")
1516

1617
def receive_message(sqs_client, queue_url):
@@ -21,8 +22,7 @@ def receive_message(sqs_client, queue_url):
2122
WaitTimeSeconds=(int (LONG_POLL_TIME)))
2223

2324
if len(response.get('Messages', [])) > 0:
24-
print(f"Number of messages received: {len(response.get('Messages', []))}")
25-
stdout.flush()
25+
logger.info(f"Number of messages received: {len(response.get('Messages', []))}")
2626
return response
2727

2828
def delete_message(sqs_client, queue_url, receipt_handle):
@@ -33,29 +33,45 @@ def delete_messages(sqs_client, queue_url, messages):
3333
receipt_handle = message['ReceiptHandle']
3434
delete_message(sqs_client=sqs_client, queue_url=queue_url, receipt_handle=receipt_handle)
3535

36-
def process_messages(topic, messages):
36+
def process_messages(sns_client, topic, messages, access_control):
3737
for message in messages.get("Messages", []):
38+
39+
# Get the permission for the collection from access-control
40+
# response = access_control.get_permissions(subscriber-id, collection-concept-id)
41+
# Return is either None (Null or Nil) (if check on response is false) or
42+
# {"C1200484253-CMR_ONLY":["read","update","delete","order"]}
43+
#if response and if array contains read:
44+
# publish message.
45+
#else:
46+
# log subscriber-id no longer has read access to collection-concept-id
47+
3848
sns_client.publish_message(topic, message)
3949

4050
def poll_queue(running):
4151
""" Poll the SQS queue and process messages. """
52+
53+
sqs_client = boto3.client("sqs", region_name=AWS_REGION)
54+
sns_resource = boto3.resource("sns", region_name=AWS_REGION)
55+
sns_client = Sns(sns_resource)
56+
topic = sns_client.create_topic(SNS_NAME)
57+
58+
access_control = AccessControl()
4259
while running.value:
4360
try:
4461
# Poll the SQS
4562
messages = receive_message(sqs_client=sqs_client, queue_url=QUEUE_URL)
4663

4764
if messages:
48-
process_messages(topic=topic, messages=messages)
65+
process_messages(sns_client=sns_client, topic=topic, messages=messages, access_control=access_control)
4966
delete_messages(sqs_client=sqs_client, queue_url=QUEUE_URL, messages=messages)
5067

5168
dl_messages = receive_message(sqs_client=sqs_client, queue_url=DEAD_LETTER_QUEUE_URL)
5269
if dl_messages:
53-
process_messages(topic=topic, messages=dl_messages)
70+
process_messages(sns_client=sns_client, topic=topic, messages=dl_messages, access_control=access_control)
5471
delete_messages(sqs_client=sqs_client, queue_url=DEAD_LETTER_QUEUE_URL, messages=dl_messages)
5572

5673
except Exception as e:
57-
print(f"An error occurred receiving or deleting messages: {e}")
58-
stdout.flush()
74+
logger.warning(f"An error occurred receiving or deleting messages: {e}")
5975

6076
app = Flask(__name__)
6177
@app.route('/shutdown', methods=['POST'])
@@ -66,16 +82,11 @@ def shutdown():
6682
running.value = False
6783
return jsonify({'status': 'shutting down'})
6884

69-
sqs_client = boto3.client("sqs", region_name=AWS_REGION)
70-
sns_resource = boto3.resource("sns")
71-
sns_client = Sns(sns_resource)
72-
topic = sns_client.create_topic(SNS_NAME)
73-
7485
#Shared boolean value for process communication
7586
running = multiprocessing.Value('b',True)
7687

7788
if __name__ == "__main__":
78-
print("Starting to poll the SQS queue...")
89+
logger.info("The subscription worker is starting to poll the SQS queue...")
7990
# Start the polling process
8091
poll_process = multiprocessing.Process(target=poll_queue, args=(running,))
8192
poll_process.start()
@@ -86,4 +97,4 @@ def shutdown():
8697

8798
# Wait for the polling process to finish before exiting
8899
poll_process.join()
89-
print("Exited polling loop.")
100+
logger.info("The subscription worker exited the polling loop.")

0 commit comments

Comments
 (0)