1
0
mirror of https://github.com/sasjs/server.git synced 2025-12-10 19:34:34 +00:00

Compare commits

..

12 Commits

Author SHA1 Message Date
semantic-release-bot
70c3834022 chore(release): 0.33.2 [skip ci]
## [0.33.2](https://github.com/sasjs/server/compare/v0.33.1...v0.33.2) (2023-04-24)

### Bug Fixes

* removing print redirection pending full [#274](https://github.com/sasjs/server/issues/274) fix ([d49ea47](d49ea47bd7))
2023-04-24 21:13:55 +00:00
Allan Bowe
dbf6c7de08 Merge pull request #355 from sasjs/issue274
fix: removing print redirection pending full #274 fix
2023-04-24 21:59:41 +01:00
allan
d49ea47bd7 fix: removing print redirection pending full #274 fix 2023-04-24 21:58:13 +01:00
semantic-release-bot
be4951d112 chore(release): 0.33.1 [skip ci]
## [0.33.1](https://github.com/sasjs/server/compare/v0.33.0...v0.33.1) (2023-04-20)

### Bug Fixes

* applying nologo only for sas.exe ([b4436ba](b4436bad0d)), closes [#352](https://github.com/sasjs/server/issues/352)
2023-04-20 08:26:33 +00:00
Allan Bowe
c116b263d9 Merge pull request #353 from sasjs/issue352
fix: applying nologo only for sas.exe
2023-04-20 09:22:29 +01:00
allan
b4436bad0d fix: applying nologo only for sas.exe
Closes #352
2023-04-20 09:16:22 +01:00
semantic-release-bot
5e325522f4 chore(release): 0.33.0 [skip ci]
# [0.33.0](https://github.com/sasjs/server/compare/v0.32.0...v0.33.0) (2023-04-05)

### Features

* option to reset admin password on startup ([eda8e56](eda8e56bb0))
2023-04-05 22:07:50 +00:00
Allan Bowe
e576fad8f4 Merge pull request #350 from sasjs/issue-348
feat: option to reset admin password on startup
2023-04-05 23:03:21 +01:00
eda8e56bb0 feat: option to reset admin password on startup 2023-04-05 23:05:38 +05:00
semantic-release-bot
bee4f215d2 chore(release): 0.32.0 [skip ci]
# [0.32.0](https://github.com/sasjs/server/compare/v0.31.0...v0.32.0) (2023-04-05)

### Features

* add an api endpoint for admin to get list of client ids ([6ffaa7e](6ffaa7e9e2))
2023-04-05 09:44:13 +00:00
Allan Bowe
100f138f98 Merge pull request #349 from sasjs/issue-347
feat: add an api endpoint for admin to get list of client ids
2023-04-05 10:39:01 +01:00
6ffaa7e9e2 feat: add an api endpoint for admin to get list of client ids 2023-04-04 23:57:01 +05:00
11 changed files with 306 additions and 31 deletions

View File

@@ -1,3 +1,31 @@
## [0.33.2](https://github.com/sasjs/server/compare/v0.33.1...v0.33.2) (2023-04-24)
### Bug Fixes
* removing print redirection pending full [#274](https://github.com/sasjs/server/issues/274) fix ([d49ea47](https://github.com/sasjs/server/commit/d49ea47bd7a2add42bdb9a717082201f29e16597))
## [0.33.1](https://github.com/sasjs/server/compare/v0.33.0...v0.33.1) (2023-04-20)
### Bug Fixes
* applying nologo only for sas.exe ([b4436ba](https://github.com/sasjs/server/commit/b4436bad0d24d5b5a402272632db1739b1018c90)), closes [#352](https://github.com/sasjs/server/issues/352)
# [0.33.0](https://github.com/sasjs/server/compare/v0.32.0...v0.33.0) (2023-04-05)
### Features
* option to reset admin password on startup ([eda8e56](https://github.com/sasjs/server/commit/eda8e56bb0ea20fdaacabbbe7dcf1e3ea7bd215a))
# [0.32.0](https://github.com/sasjs/server/compare/v0.31.0...v0.32.0) (2023-04-05)
### Features
* add an api endpoint for admin to get list of client ids ([6ffaa7e](https://github.com/sasjs/server/commit/6ffaa7e9e2a62c083bb9fcc3398dcbed10cebdb1))
# [0.31.0](https://github.com/sasjs/server/compare/v0.30.3...v0.31.0) (2023-03-30)

View File

@@ -188,6 +188,19 @@ MAX_WRONG_ATTEMPTS_BY_IP_PER_DAY = <number> default: 100;
# Once a successful login is attempted, it resets
MAX_CONSECUTIVE_FAILS_BY_USERNAME_AND_IP = <number> default: 10;
# Name of the admin user that will be created on startup if not exists already
# Default is `secretuser`
ADMIN_USERNAME=secretuser
# Temporary password for the ADMIN_USERNAME, which is in place until the first login
# Default is `secretpassword`
ADMIN_PASSWORD_INITIAL=secretpassword
# Specify whether app has to reset the ADMIN_USERNAME's password or not
# Default is NO. Possible options are YES and NO
# If ADMIN_PASSWORD_RESET is YES then the ADMIN_USERNAME will be prompted to change the password from ADMIN_PASSWORD_INITIAL on their next login. This will repeat on every server restart, unless the option is removed / set to NO.
ADMIN_PASSWORD_RESET=NO
# LOG_FORMAT_MORGAN options: [combined|common|dev|short|tiny] default: `common`
# Docs: https://www.npmjs.com/package/morgan#predefined-formats
LOG_FORMAT_MORGAN=

View File

@@ -30,6 +30,10 @@ MAX_WRONG_ATTEMPTS_BY_IP_PER_DAY=100
#default value is 10
MAX_CONSECUTIVE_FAILS_BY_USERNAME_AND_IP=10
ADMIN_USERNAME=secretuser
ADMIN_PASSWORD_INITIAL=secretpassword
ADMIN_PASSWORD_RESET=NO
RUN_TIMES=[sas,js,py | js,py | sas | sas,js] default considered as sas
SAS_PATH=/opt/sas/sas9/SASHome/SASFoundation/9.4/sas
NODE_PATH=~/.nvm/versions/node/v16.14.0/bin/node

View File

@@ -758,6 +758,27 @@ paths:
application/json:
schema:
$ref: '#/components/schemas/ClientPayload'
get:
operationId: GetAllClients
responses:
'200':
description: Ok
content:
application/json:
schema:
items:
$ref: '#/components/schemas/ClientPayload'
type: array
examples:
'Example 1':
value: [{clientId: someClientID1234, clientSecret: someRandomCryptoString, accessTokenExpiration: 86400}, {clientId: someOtherClientID, clientSecret: someOtherRandomCryptoString, accessTokenExpiration: 86400}]
summary: 'Admin only task. Returns the list of all the clients *'
tags:
- Client
security:
-
bearerAuth: []
parameters: []
/SASjsApi/code/execute:
post:
operationId: ExecuteCode

View File

@@ -1,4 +1,4 @@
import { Security, Route, Tags, Example, Post, Body } from 'tsoa'
import { Security, Route, Tags, Example, Post, Body, Get } from 'tsoa'
import Client, {
ClientPayload,
@@ -29,6 +29,28 @@ export class ClientController {
): Promise<ClientPayload> {
return createClient(body)
}
/**
* @summary Admin only task. Returns the list of all the clients
*/
@Example<ClientPayload[]>([
{
clientId: 'someClientID1234',
clientSecret: 'someRandomCryptoString',
accessTokenExpiration: NUMBER_OF_SECONDS_IN_A_DAY,
refreshTokenExpiration: NUMBER_OF_SECONDS_IN_A_DAY * 30
},
{
clientId: 'someOtherClientID',
clientSecret: 'someOtherRandomCryptoString',
accessTokenExpiration: NUMBER_OF_SECONDS_IN_A_DAY,
refreshTokenExpiration: NUMBER_OF_SECONDS_IN_A_DAY * 30
}
])
@Get('/')
public async getAllClients(): Promise<ClientPayload[]> {
return getAllClients()
}
}
const createClient = async (data: ClientPayload): Promise<ClientPayload> => {
@@ -60,3 +82,13 @@ const createClient = async (data: ClientPayload): Promise<ClientPayload> => {
refreshTokenExpiration: savedClient.refreshTokenExpiration
}
}
const getAllClients = async (): Promise<ClientPayload[]> => {
return Client.find({}).select({
_id: 0,
clientId: 1,
clientSecret: 1,
accessTokenExpiration: 1,
refreshTokenExpiration: 1
})
}

View File

@@ -134,7 +134,7 @@ ${autoExecContent}`
session.path,
'-AUTOEXEC',
autoExecPath,
isWindows() ? '-nologo' : '',
process.sasLoc!.endsWith('sas.exe') ? '-nologo' : '',
process.sasLoc!.endsWith('sas.exe') ? '-nosplash' : '',
process.sasLoc!.endsWith('sas.exe') ? '-icon' : '',
process.sasLoc!.endsWith('sas.exe') ? '-nodms' : '',

View File

@@ -40,8 +40,6 @@ export const createSASProgram = async (
%mend;
%_sasjs_server_init()
proc printto print="%sysfunc(getoption(log))";
run;
`
program = `

View File

@@ -1,6 +1,7 @@
import express from 'express'
import { ClientController } from '../../controllers'
import { registerClientValidation } from '../../utils'
import { authenticateAccessToken, verifyAdmin } from '../../middlewares'
const clientRouter = express.Router()
@@ -17,4 +18,19 @@ clientRouter.post('/', async (req, res) => {
}
})
clientRouter.get(
'/',
authenticateAccessToken,
verifyAdmin,
async (req, res) => {
const controller = new ClientController()
try {
const response = await controller.getAllClients()
res.send(response)
} catch (err: any) {
res.status(403).send(err.toString())
}
}
)
export default clientRouter

View File

@@ -5,6 +5,7 @@ import request from 'supertest'
import appPromise from '../../../app'
import { UserController, ClientController } from '../../../controllers/'
import { generateAccessToken, saveTokensInDB } from '../../../utils'
import { NUMBER_OF_SECONDS_IN_A_DAY } from '../../../model/Client'
const client = {
clientId: 'someclientID',
@@ -26,6 +27,7 @@ describe('client', () => {
let app: Express
let con: Mongoose
let mongoServer: MongoMemoryServer
let adminAccessToken: string
const userController = new UserController()
const clientController = new ClientController()
@@ -34,6 +36,18 @@ describe('client', () => {
mongoServer = await MongoMemoryServer.create()
con = await mongoose.connect(mongoServer.getUri())
const dbUser = await userController.createUser(adminUser)
adminAccessToken = generateAccessToken({
clientId: client.clientId,
userId: dbUser.id
})
await saveTokensInDB(
dbUser.id,
client.clientId,
adminAccessToken,
'refreshToken'
)
})
afterAll(async () => {
@@ -43,22 +57,6 @@ describe('client', () => {
})
describe('create', () => {
let adminAccessToken: string
beforeAll(async () => {
const dbUser = await userController.createUser(adminUser)
adminAccessToken = generateAccessToken({
clientId: client.clientId,
userId: dbUser.id
})
await saveTokensInDB(
dbUser.id,
client.clientId,
adminAccessToken,
'refreshToken'
)
})
afterEach(async () => {
const collections = mongoose.connection.collections
const collection = collections['clients']
@@ -157,4 +155,80 @@ describe('client', () => {
expect(res.body).toEqual({})
})
})
describe('get', () => {
afterEach(async () => {
const collections = mongoose.connection.collections
const collection = collections['clients']
await collection.deleteMany({})
})
it('should respond with an array of all clients', async () => {
await clientController.createClient(newClient)
await clientController.createClient({
clientId: 'clientID',
clientSecret: 'clientSecret'
})
const res = await request(app)
.get('/SASjsApi/client')
.auth(adminAccessToken, { type: 'bearer' })
.send()
.expect(200)
const expected = [
{
clientId: 'newClientID',
clientSecret: 'newClientSecret',
accessTokenExpiration: NUMBER_OF_SECONDS_IN_A_DAY,
refreshTokenExpiration: NUMBER_OF_SECONDS_IN_A_DAY * 30
},
{
clientId: 'clientID',
clientSecret: 'clientSecret',
accessTokenExpiration: NUMBER_OF_SECONDS_IN_A_DAY,
refreshTokenExpiration: NUMBER_OF_SECONDS_IN_A_DAY * 30
}
]
expect(res.body).toEqual(expected)
})
it('should respond with Unauthorized if access token is not present', async () => {
const res = await request(app).get('/SASjsApi/client').send().expect(401)
expect(res.text).toEqual('Unauthorized')
expect(res.body).toEqual({})
})
it('should respond with Forbideen if access token is not of an admin account', async () => {
const user = {
displayName: 'User 2',
username: 'username2',
password: '12345678',
isAdmin: false,
isActive: true
}
const dbUser = await userController.createUser(user)
const accessToken = generateAccessToken({
clientId: client.clientId,
userId: dbUser.id
})
await saveTokensInDB(
dbUser.id,
client.clientId,
accessToken,
'refreshToken'
)
const res = await request(app)
.get('/SASjsApi/client')
.auth(accessToken, { type: 'bearer' })
.send()
.expect(401)
expect(res.text).toEqual('Admin account required')
expect(res.body).toEqual({})
})
})
})

View File

@@ -1,7 +1,9 @@
import bcrypt from 'bcryptjs'
import Client from '../model/Client'
import Group, { PUBLIC_GROUP_NAME } from '../model/Group'
import User from '../model/User'
import User, { IUser } from '../model/User'
import Configuration, { ConfigurationType } from '../model/Configuration'
import { ResetAdminPasswordType } from './verifyEnvVariables'
import { randomBytes } from 'crypto'
@@ -40,9 +42,13 @@ export const seedDB = async (): Promise<ConfigurationType> => {
process.logger.success(`DB Seed - Group created: ${PUBLIC_GROUP.name}`)
}
const ADMIN_USER = getAdminUser()
// Checking if user is already in the database
let usernameExist = await User.findOne({ username: ADMIN_USER.username })
if (!usernameExist) {
if (usernameExist) {
usernameExist = await resetAdminPassword(usernameExist, ADMIN_USER.password)
} else {
const user = new User(ADMIN_USER)
usernameExist = await user.save()
@@ -51,7 +57,7 @@ export const seedDB = async (): Promise<ConfigurationType> => {
)
}
if (!groupExist.hasUser(usernameExist)) {
if (usernameExist.isAdmin && !groupExist.hasUser(usernameExist)) {
groupExist.addUser(usernameExist)
process.logger.success(
`DB Seed - admin account '${ADMIN_USER.username}' added to Group '${ALL_USERS_GROUP.name}'`
@@ -90,11 +96,52 @@ const CLIENT = {
clientId: 'clientID1',
clientSecret: 'clientSecret'
}
const ADMIN_USER = {
id: 1,
displayName: 'Super Admin',
username: 'secretuser',
password: '$2a$10$hKvcVEZdhEQZCcxt6npazO6mY4jJkrzWvfQ5stdBZi8VTTwVMCVXO',
isAdmin: true,
isActive: true
const getAdminUser = () => {
const { ADMIN_USERNAME, ADMIN_PASSWORD_INITIAL } = process.env
const salt = bcrypt.genSaltSync(10)
const hashedPassword = bcrypt.hashSync(ADMIN_PASSWORD_INITIAL as string, salt)
return {
displayName: 'Super Admin',
username: ADMIN_USERNAME,
password: hashedPassword,
isAdmin: true,
isActive: true
}
}
const resetAdminPassword = async (user: IUser, password: string) => {
const { ADMIN_PASSWORD_RESET } = process.env
if (ADMIN_PASSWORD_RESET === ResetAdminPasswordType.YES) {
if (!user.isAdmin) {
process.logger.error(
`Can not reset the password of non-admin user (${user.username}) on startup.`
)
return user
}
if (user.authProvider) {
process.logger.error(
`Can not reset the password of admin (${user.username}) with ${user.authProvider} as authentication mechanism.`
)
return user
}
process.logger.info(
`DB Seed - resetting password for admin user: ${user.username}`
)
user.password = password
user.needsToUpdatePassword = true
user = await user.save()
process.logger.success(`DB Seed - successfully reset the password`)
}
return user
}

View File

@@ -52,6 +52,11 @@ export enum DatabaseType {
COSMOS_MONGODB = 'cosmos_mongodb'
}
export enum ResetAdminPasswordType {
YES = 'YES',
NO = 'NO'
}
export const verifyEnvVariables = (): ReturnCode => {
const errors: string[] = []
@@ -79,6 +84,8 @@ export const verifyEnvVariables = (): ReturnCode => {
errors.push(...verifyRateLimiter())
errors.push(...verifyAdminUserConfig())
if (errors.length) {
process.logger?.error(
`Invalid environment variable(s) provided: \n${errors.join('\n')}`
@@ -409,6 +416,38 @@ const verifyRateLimiter = () => {
return errors
}
const verifyAdminUserConfig = () => {
const errors: string[] = []
const { MODE, ADMIN_USERNAME, ADMIN_PASSWORD_INITIAL, ADMIN_PASSWORD_RESET } =
process.env
if (MODE === ModeType.Server) {
if (ADMIN_USERNAME) {
process.env.ADMIN_USERNAME = ADMIN_USERNAME.toLowerCase()
} else {
process.env.ADMIN_USERNAME = DEFAULTS.ADMIN_USERNAME
}
if (!ADMIN_PASSWORD_INITIAL)
process.env.ADMIN_PASSWORD_INITIAL = DEFAULTS.ADMIN_PASSWORD_INITIAL
if (ADMIN_PASSWORD_RESET) {
const resetPasswordTypes = Object.values(ResetAdminPasswordType)
if (
!resetPasswordTypes.includes(
ADMIN_PASSWORD_RESET as ResetAdminPasswordType
)
)
errors.push(
`- ADMIN_PASSWORD_RESET '${ADMIN_PASSWORD_RESET}'\n - valid options ${resetPasswordTypes}`
)
} else {
process.env.ADMIN_PASSWORD_RESET = DEFAULTS.ADMIN_PASSWORD_RESET
}
}
return errors
}
const isNumeric = (val: string): boolean => {
return !isNaN(Number(val))
}
@@ -422,5 +461,8 @@ const DEFAULTS = {
RUN_TIMES: RunTimeType.SAS,
DB_TYPE: DatabaseType.MONGO,
MAX_WRONG_ATTEMPTS_BY_IP_PER_DAY: '100',
MAX_CONSECUTIVE_FAILS_BY_USERNAME_AND_IP: '10'
MAX_CONSECUTIVE_FAILS_BY_USERNAME_AND_IP: '10',
ADMIN_USERNAME: 'secretuser',
ADMIN_PASSWORD_INITIAL: 'secretpassword',
ADMIN_PASSWORD_RESET: ResetAdminPasswordType.NO
}