Skip to content

Commit b581975

Browse files
authored
Upgrade the app to latest SDK and node version (#37)
* update packages * update TF and toolchain * update TF and toolchain * update TF and toolchain * improved docs * switch away from Alpine * review comments
1 parent 53773c1 commit b581975

File tree

9 files changed

+1627
-1007
lines changed

9 files changed

+1627
-1007
lines changed

.tekton/pipelines.yaml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ spec:
1414
- name: registry-namespace
1515
- name: image-name
1616
- name: registry-region
17+
- name: fail-on-scanned-issues
1718
resources:
1819
- name: app-image
1920
type: image
@@ -78,6 +79,8 @@ spec:
7879
value: $(tasks.build.results.image-repository)
7980
- name: image-digest
8081
value: $(tasks.build.results.image-digest)
82+
- name: fail-on-scanned-issues
83+
value: $(params.fail-on-scanned-issues)
8184
workspaces:
8285
- name: artifacts
8386
workspace: pipeline-ws

.tekton/triggers.yaml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ spec:
1414
- name: registry-namespace
1515
- name: registry-region
1616
- name: image-name
17+
- name: fail-on-scanned-issues
18+
description: fail if image determined insecure
1719
resourcetemplates:
1820
- apiVersion: v1
1921
kind: PersistentVolumeClaim
@@ -52,6 +54,8 @@ spec:
5254
value: $(params.registry-region)
5355
- name: image-name
5456
value: $(params.image-name)
57+
- name: fail-on-scanned-issues
58+
value: $(params.fail-on-scanned-issues)
5559
workspaces:
5660
- name: pipeline-ws
5761
persistentVolumeClaim:

README.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,14 +105,17 @@ Located in the [app](app) directory:
105105

106106

107107
### To test locally
108+
The app can be tested and developed locally, however it requires a version (same or different to the local version) of the app to be deployed in Kubernetes. The reason is that access is guarded by an access token. That token can only be issued in the Kubernetes environment with App ID intercepting requests.
108109

110+
To test locally:
109111
1. Follow the tutorial instructions to have the app deployed to a cluster. Specially the sections to create all the services and to populate the `credentials.env` file. You will need the public instead of the private COS endpoint in order to access Cloud Object Storage from your machine.
110112
1. Access the tokens with `https://secure-file-storage.<INGRESS_SUBDOMAIN>/api/tokens`. This will shows the raw App ID authorization header together with the decode JWT tokens for your session.
111113
1. In your local shell:
112114
```
113115
export TEST_AUTHORIZATION_HEADER="<value of the header attribute 'Bearer ... ...'>"
114116
```
115-
1. npm start
117+
1. `npm start` or `node app.js` to start the app.
118+
1. Access the local app through the shown URL. Now, you can change the app source code and test locally.
116119

117120

118121
## License

app/Dockerfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
FROM node:14-alpine
1+
FROM node:20
22
ENV NODE_ENV production
33
WORKDIR /usr/src/app
44
COPY . .

app/app.js

Lines changed: 132 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,12 @@
1+
// Sample app to store and access files securely
2+
//
3+
// The app provides a simple web-based service to upload, store, and access
4+
// files. Files can be shared via an expiring file link.
5+
// The app uses IBM Cloudant to store file metadata and IBM Cloud Object Storage
6+
// for the actual file object.
7+
//
8+
// The API functions are called from client-side JavaScript
9+
110
var express = require('express'),
211
formidable = require('formidable'),
312
cookieParser = require('cookie-parser'),
@@ -12,30 +21,38 @@ require('dotenv').config({
1221

1322
var allowAnonymousAccess = process.env.allow_anonymous || false;
1423

24+
// some values taken from the environment
25+
const CLOUDANT_APIKEY = process.env.cloudant_iam_apikey;
26+
const CLOUDANT_URL = process.env.cloudant_url;
27+
const CLOUDANT_DB = process.env.cloudant_database || 'secure-file-storage-metadata';
28+
29+
const COS_BUCKET_NAME = process.env.cos_bucket_name;
30+
const COS_ENDPOINT = process.env.cos_endpoint;
31+
const COS_APIKEY = process.env.cos_apiKey;
32+
const COS_IAM_AUTH_ENDPOINT = process.env.cos_ibmAuthEndpoint || 'https://iam.cloud.ibm.com/identity/token';
33+
const COS_INSTANCE_ID = process.env.cos_resourceInstanceID;
34+
const COS_ACCESS_KEY_ID = process.env.cos_access_key_id;
35+
const COS_SECRET_ACCESS_KEY = process.env.cos_secret_access_key;
36+
1537
// Initialize Cloudant
16-
var Cloudant = require('@cloudant/cloudant');
17-
var cloudant = new Cloudant({
18-
url: process.env.cloudant_url,
19-
plugins: [
20-
'promises',
21-
{
22-
iamauth: {
23-
iamApiKey: process.env.cloudant_iam_apikey
24-
}
25-
}
26-
]
38+
const { IamAuthenticator } = require('ibm-cloud-sdk-core');
39+
const authenticator = new IamAuthenticator({
40+
apikey: CLOUDANT_APIKEY
2741
});
28-
var db = cloudant.db.use(process.env.cloudant_database || 'secure-file-storage-metadata');
2942

30-
var CloudObjectStorage = require('ibm-cos-sdk');
43+
const { CloudantV1 } = require('@ibm-cloud/cloudant');
44+
45+
const cloudant = CloudantV1.newInstance({ authenticator: authenticator });
46+
cloudant.setServiceUrl(CLOUDANT_URL);
3147

3248
// Initialize the COS connection.
49+
var CloudObjectStorage = require('ibm-cos-sdk');
3350
// This connection is used when interacting with the bucket from the app to upload/delete files.
3451
var config = {
35-
endpoint: process.env.cos_endpoint,
36-
apiKeyId: process.env.cos_apiKey,
37-
ibmAuthEndpoint: process.env.cos_ibmAuthEndpoint || 'https://iam.cloud.ibm.com/identity/token',
38-
serviceInstanceId: process.env.cos_resourceInstanceID,
52+
endpoint: COS_ENDPOINT,
53+
apiKeyId: COS_APIKEY,
54+
ibmAuthEndpoint: COS_IAM_AUTH_ENDPOINT,
55+
serviceInstanceId: COS_INSTANCE_ID,
3956
};
4057
var cos = new CloudObjectStorage.S3(config);
4158

@@ -44,7 +61,7 @@ var cos = new CloudObjectStorage.S3(config);
4461
// able to access the content from their own computer.
4562
//
4663
// We derive the COS public endpoint from what should be the private/direct endpoint.
47-
let cosPublicEndpoint = process.env.cos_endpoint;
64+
let cosPublicEndpoint = COS_ENDPOINT;
4865
if (cosPublicEndpoint.startsWith('s3.private')) {
4966
cosPublicEndpoint = `s3${cosPublicEndpoint.substring('s3.private'.length)}`;
5067
} else if (cosPublicEndpoint.startsWith('s3.direct')) {
@@ -55,16 +72,15 @@ console.log('Public endpoint for COS is', cosPublicEndpoint);
5572
var cosUrlGenerator = new CloudObjectStorage.S3({
5673
endpoint: cosPublicEndpoint,
5774
credentials: new CloudObjectStorage.Credentials(
58-
process.env.cos_access_key_id,
59-
process.env.cos_secret_access_key, sessionToken = null),
75+
COS_ACCESS_KEY_ID,
76+
COS_SECRET_ACCESS_KEY, sessionToken = null),
6077
signatureVersion: 'v4',
6178
});
6279

63-
const COS_BUCKET_NAME = process.env.cos_bucket_name;
64-
65-
// Define routes
80+
// Simple Express setup
6681
var app = express();
6782
app.use(cookieParser());
83+
// Define routes
6884
app.use('/', express.static(__dirname + '/public'));
6985

7086
// Decodes access and identity tokens sent by App ID in the Authorization header
@@ -114,6 +130,7 @@ app.use('/api/', (req, res, next) => {
114130
}
115131
});
116132

133+
// Extract the subject out of the access token
117134
function getSub(req) {
118135
if (req.appIdAuthorizationContext) {
119136
return req.appIdAuthorizationContext.access_token.sub;
@@ -126,38 +143,52 @@ function getSub(req) {
126143

127144
// Returns all files associated to the current user
128145
app.get('/api/files', async function (req, res) {
129-
try {
130-
const body = await db.find({
131-
selector: {
132-
userId: getSub(req),
133-
}
134-
});
135-
res.send(body.docs.map(function (item) {
146+
// filter on the userId which is the subject in the access token
147+
const selector = {
148+
userId: {
149+
'$eq': getSub(req)
150+
}
151+
};
152+
// Cloudant API to find documents
153+
cloudant.postFind({
154+
db: CLOUDANT_DB,
155+
selector: selector,
156+
}).then(response => {
157+
// remove some metadata
158+
res.send(response.result.docs.map(function (item) {
136159
item.id = item._id
137160
delete item._id;
138161
delete item._rev;
139162
return item;
140163
}));
141-
} catch (err) {
142-
console.log(err);
143-
res.status(500).send(err);
164+
}).catch(error => {
165+
console.log(error.status, error.message);
166+
res.status(500).send(error.message);
144167
}
168+
);
145169
});
146170

171+
147172
// Generates a pre-signed URL to access a file owned by the current user
148173
app.get('/api/files/:id/url', async function (req, res) {
149-
try {
150-
const result = await db.find({
151-
selector: {
152-
_id: req.params.id,
153-
userId: getSub(req),
154-
}
155-
});
156-
if (result.docs.length === 0) {
174+
const selector = {
175+
userId: {
176+
'$eq': getSub(req)
177+
},
178+
_id: {
179+
'$eq': req.params.id,
180+
}
181+
};
182+
// Cloudant API to find documents
183+
cloudant.postFind({
184+
db: CLOUDANT_DB,
185+
selector: selector,
186+
}).then(response => {
187+
if (response.result.docs.length === 0) {
157188
res.status(404).send({ message: 'Document not found' });
158189
return;
159190
}
160-
const doc = result.docs[0];
191+
const doc = response.result.docs[0];
161192
const url = cosUrlGenerator.getSignedUrl('getObject', {
162193
Bucket: COS_BUCKET_NAME,
163194
Key: `${doc.userId}/${doc._id}/${doc.name}`,
@@ -166,10 +197,10 @@ app.get('/api/files/:id/url', async function (req, res) {
166197

167198
console.log(`[OK] Built signed url for ${req.params.id}`);
168199
res.send({ url });
169-
} catch (err) {
200+
}).catch(error => {
170201
console.log(`[KO] Could not retrieve document ${req.params.id}`, err);
171202
res.status(500).send(err);
172-
}
203+
});
173204
});
174205

175206
// Uploads files, associating them to the current user
@@ -191,69 +222,90 @@ app.post('/api/files', function (req, res) {
191222
userId: getSub(req),
192223
};
193224

194-
try {
195-
console.log(`New file to upload: ${fileDetails.name} (${fileDetails.size} bytes)`);
225+
console.log(`New file to upload: ${fileDetails.name} (${fileDetails.size} bytes)`);
196226

197-
// create Cloudant document
198-
const doc = await db.insert(fileDetails);
199-
fileDetails.id = doc.id;
227+
// create Cloudant document
228+
cloudant.postDocument({
229+
db: CLOUDANT_DB,
230+
document: fileDetails
231+
}).then(async response => {
232+
console.log(response);
233+
fileDetails.id = response.result.id;
200234

201235
// upload to COS
202236
await cos.upload({
203237
Bucket: COS_BUCKET_NAME,
204238
Key: `${fileDetails.userId}/${fileDetails.id}/${fileDetails.name}`,
205239
Body: fs.createReadStream(file.path),
206240
ContentType: fileDetails.type,
207-
}).promise();
241+
}).promise()
208242

209243
// reply with the document
210244
console.log(`[OK] Document ${fileDetails.id} uploaded to storage`);
211245
res.send(fileDetails);
212-
} catch (err) {
213-
console.log(`[KO] Failed to upload ${fileDetails.name}`, err);
214-
res.status(500).send(err);
215-
}
216-
217-
// delete the file once uploaded
218-
fs.unlink(file.path, (err) => {
219-
if (err) { console.log(err) }
246+
// delete the file once uploaded
247+
fs.unlink(file.path, (err) => {
248+
if (err) { console.log(err) }
249+
});
250+
}).catch(error => {
251+
console.log(`[KO] Failed to upload ${fileDetails.name}`, error.message);
252+
res.status(500).send(error.status, error.message);
220253
});
221254
});
222255
});
223256

257+
224258
// Deletes a file associated with the current user
225259
app.delete('/api/files/:id', async function (req, res) {
226-
try {
227-
console.log(`Deleting document ${req.params.id}`);
228-
229-
// get the doc from cloudant, ensuring it is owned by the current user
230-
const result = await db.find({
231-
selector: {
232-
_id: req.params.id,
233-
userId: getSub(req),
234-
}
235-
});
236-
if (result.docs.length === 0) {
260+
261+
console.log(`Deleting document ${req.params.id}`);
262+
// get the doc from cloudant, ensuring it is owned by the current user
263+
// filter on the userId which is the subject in the access token
264+
// AND the document ID
265+
const selector = {
266+
userId: {
267+
'$eq': getSub(req)
268+
},
269+
_id: {
270+
'$eq': req.params.id,
271+
}
272+
};
273+
// Cloudant API to find documents
274+
cloudant.postFind({
275+
db: CLOUDANT_DB,
276+
selector: selector
277+
}).then(response => {
278+
if (response.result.docs.length === 0) {
237279
res.status(404).send({ message: 'Document not found' });
238280
return;
239281
}
240-
const doc = result.docs[0];
241-
282+
const doc = response.result.docs[0];
242283
// remove the COS object
243284
console.log(`Removing file ${doc.userId}/${doc._id}/${doc.name}`);
244-
await cos.deleteObject({
285+
286+
cos.deleteObject({
245287
Bucket: COS_BUCKET_NAME,
246288
Key: `${doc.userId}/${doc._id}/${doc.name}`
247289
}).promise();
248290

249291
// remove the cloudant object
250-
await db.destroy(doc._id, doc._rev);
292+
cloudant.deleteDocument({
293+
db: CLOUDANT_DB,
294+
docId: doc._id,
295+
rev: doc._rev
296+
}).then(response => {
297+
console.log(`[OK] Successfully deleted ${doc._id}`);
298+
res.status(204).send();
299+
}).catch(error => {
300+
console.log(error.status, error.message);
301+
res.status(500).send(error.message);
302+
});
303+
304+
}).catch(error => {
305+
console.log(error.status, error.message);
306+
res.status(500).send(error.message);
307+
});
251308

252-
console.log(`[OK] Successfully deleted ${doc._id}`);
253-
res.status(204).send();
254-
} catch (err) {
255-
res.status(500).send(err);
256-
}
257309
});
258310

259311
// Called by App ID when the authorization flow completes
@@ -265,7 +317,7 @@ app.get('/api/tokens', function (req, res) {
265317
res.send(req.appIdAuthorizationContext);
266318
});
267319

268-
app.get('/api/user', function(req, res) {
320+
app.get('/api/user', function (req, res) {
269321
let result = {};
270322
if (req.appIdAuthorizationContext) {
271323
result = {
@@ -280,6 +332,7 @@ app.get('/api/user', function(req, res) {
280332
res.send(result);
281333
});
282334

335+
// start the server
283336
const server = app.listen(process.env.PORT || 8081, () => {
284337
console.log(`Listening on port http://0.0.0.0:${server.address().port}`);
285338
});

0 commit comments

Comments
 (0)