Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Wip #2

Open
wants to merge 28 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
cf2c3f3
Add rule-compiler.js from ZeroTierOne before its relicensing
lideming Mar 1, 2021
08a2837
Rename network property pages to have prefix
lideming Mar 2, 2021
2ecd5e6
Change storage structure
lideming Mar 2, 2021
551f30c
Make "rule-compiler.js" be able to run in browsers
lideming Mar 9, 2021
f9922e2
Add missing "member_prop_"
lideming Mar 9, 2021
ebbc117
Improve storage upgrading
lideming Mar 9, 2021
4ecba33
Fix error page
lideming Mar 9, 2021
3474890
wip: rules
lideming Mar 9, 2021
97f62f6
Add missing "network_prop_" in render paths
lideming Jul 30, 2021
cacb259
fix IPv6 route target comparing
lideming Nov 2, 2021
742ff99
Rename network property pages to have prefix "network_prop_"
lideming Mar 1, 2021
b11cfbc
Merge branch 'fix-ipv6-route-delete' into rules
lideming Nov 10, 2021
f76ba8b
deps: Update some dependencies (#78)
key-networks Dec 4, 2021
d059636
node16: Builds on node v16.13.1 (#79)
key-networks Dec 22, 2021
b50215f
fix IPv6 route target comparing (#75)
lideming Jan 3, 2022
8fb29fa
Version bump
key-networks Jan 3, 2022
d0f9a45
fix "relay" status not showing
lideming Jan 7, 2022
98db005
fix member ip assignment
lideming Mar 4, 2022
d021cd2
get SESSION_SECRET from env
lideming Mar 4, 2022
671525e
improve peer addresses display
lideming Mar 10, 2022
4b8af3a
peer address (latency / last active)
lideming Mar 12, 2022
b5ac8e5
Update network_detail.pug
tinola May 27, 2022
4f79530
Update network_detail.pug
tinola May 27, 2022
b774939
Merge branch 'fix-relay-status' into wip
lideming Aug 17, 2022
c400228
storage typing draft
lideming Aug 17, 2022
07b9799
OIDC auth
lideming Aug 17, 2022
b57e896
wip: update readme for oidc
lideming Aug 18, 2022
b4268a3
Merge branch 'lideming:wip' into wip
tinola Aug 23, 2022
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,21 @@ Open PuTTY and configure as follows:

Once the SSH tunnel has been established, access the ztncui web interface in a web browser on your local machine at: http://localhost:3333

##### 11. OpenID Connect Login

While password login is used by default, you can enable OpenID Connect (OIDC) login by settings these options:

```dotenv
# Required:
AUTH_OIDC_ISSUER=https://myissuer.example.org
AUTH_OIDC_CLIENT_ID=
AUTH_OIDC_CLIENT_SECRET=
BASE_URL=https://myztncui.example.org

# Optional, to disable password login:
AUTH_OIDC_ONLY=true
```

## Usage
### User accounts
Once you have access to the web UI of ztncui, log in as user **admin** with password **password**.
Expand Down
2 changes: 1 addition & 1 deletion build/binding.gyp.patch
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
],
"cflags+": ["-Wno-cast-function-type"],
"include_dirs+": ["<!(node -e \"require('nan')\")"],
+ "libraries": ["/usr/lib/gcc/x86_64-redhat-linux/10/libstdc++.a"],
+ "libraries": ["/usr/lib/gcc/x86_64-redhat-linux/8/libstdc++.a"],
"dependencies": ["libargon2"],
"configurations": {
"Debug": {
6 changes: 3 additions & 3 deletions build/build.sh
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,9 @@ LICENSE='GPLv3'

BINDINGGYP='node_modules/argon2/binding.gyp'

NODE_VER='v14'
NODE_VER='v16'

if [ ! -f /usr/lib/gcc/x86_64-redhat-linux/10/libstdc++.a ]; then
if [ ! -f /usr/lib/gcc/x86_64-redhat-linux/8/libstdc++.a ]; then
echo "You must install libstdc++-static"
exit 1
fi
Expand Down Expand Up @@ -75,7 +75,7 @@ if [ $? -ne 0 ]; then
fi

popd
pkg -c ./package.json -t node14-linux-x64 bin/www -o $BUILD_DIR/ztncui
pkg -c ./package.json -t node16-linux-x64 bin/www -o $BUILD_DIR/ztncui

popd

Expand Down
3 changes: 2 additions & 1 deletion src/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ const zt_controller = require('./routes/zt_controller');

const app = express();

const session_secret = Math.random().toString(36).substring(2,12);
const session_secret = process.env.SESSION_SECRET || Math.random().toString(36).substring(2,12);

// view engine setup
app.set('views', path.join(__dirname, 'views'));
Expand Down Expand Up @@ -64,6 +64,7 @@ next();

// error handler
app.use(function(err, req, res, next) {
console.error('app error', err);
// set locals, only providing error in development
res.locals.message = err.message;
res.locals.error = req.app.get('env') === 'development' ? err : {};
Expand Down
44 changes: 26 additions & 18 deletions src/bin/www
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ const debug = require('debug')('ztncui:server');
const http = require('http');
const https = require('https');
const fs = require('fs');
const storage = require('../controllers/storage');

/**
* Get ports from environment and store in Express.
Expand Down Expand Up @@ -36,33 +37,40 @@ app.set('https_host', https_host);
* HTTPS_HOST HTTPS HTTPS_HOST HTTPS_PORT
*/

const http_all_interfaces = process.env.HTTP_ALL_INTERFACES || null;
if (http_all_interfaces) {
console.log('Listening for HTTP requests on port ' + http_port + ' on all interfaces');
app.listen(http_port);
} else {
console.log('Listening for HTTP requests on port ' + http_port + ' on localhost');
app.listen(http_port, 'localhost');
}

const options = !https_port ? {} : {
cert: fs.readFileSync('etc/tls/fullchain.pem'),
key: fs.readFileSync('etc/tls/privkey.pem')
};

const server = https.createServer(options, app);
const https_server = https.createServer(options, app);

if (https_port) {
if (https_host) {
console.log('Listening for HTTPS requests on port ' + https_port + ' on address ' + https_host);
async function start() {
await storage.init();

const http_all_interfaces = process.env.HTTP_ALL_INTERFACES || null;
if (http_all_interfaces) {
console.log('Listening for HTTP requests on port ' + http_port + ' on all interfaces');
app.listen(http_port);
} else {
console.log('Listening for HTTPS requests on port ' + https_port + ' on all interfaces');
console.log('Listening for HTTP requests on port ' + http_port + ' on localhost');
app.listen(http_port, 'localhost');
}
server.listen(https_port, https_host);

if (https_port) {
if (https_host) {
console.log('Listening for HTTPS requests on port ' + https_port + ' on address ' + https_host);
} else {
console.log('Listening for HTTPS requests on port ' + https_port + ' on all interfaces');
}
https_server.listen(https_port, https_host);
}

https_server.on('error', onError);
https_server.on('listening', onListening);
}

server.on('error', onError);
server.on('listening', onListening);
start();


/**
* Normalize a port into a number, string, or false.
Expand Down Expand Up @@ -121,7 +129,7 @@ function onError(error) {
*/

function onListening() {
const addr = server.address();
const addr = https_server.address();
const bind = typeof addr === 'string'
? 'pipe ' + addr
: 'port ' + addr.port;
Expand Down
129 changes: 125 additions & 4 deletions src/controllers/auth.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
const argon2 = require('argon2');
const usersController = require('../controllers/usersController');

const hash_check = async function(user, password) {
const hash_check = async function (user, password) {
let verified = false;
try {
var users = await usersController.get_users();
Expand All @@ -22,7 +22,17 @@ const hash_check = async function(user, password) {
return verified;
}

exports.authenticate = async function(name, pass, callback) {
exports.hasRightsForNetwork = hasRightsForNetwork;
function hasRightsForNetwork(user, nwid) {
return isSuperAdmin(user) || user.networks.includes(nwid);
}

exports.isSuperAdmin = isSuperAdmin;
function isSuperAdmin(user) {
return user.role == 'superadmin';
}

exports.authenticate = async function (name, pass, callback) {
try {
var users = await usersController.get_users();
} catch (err) {
Expand All @@ -32,17 +42,128 @@ exports.authenticate = async function(name, pass, callback) {
if (!user) return callback(new Error('cannot find user'));
let verified = await hash_check(name, pass);
if (verified) {
user = { ...user };
user.role = 'superadmin';
user.networks = [];
return callback(null, user);
} else {
return callback(new Error('invalid password'));
}
}

exports.restrict = function(req, res, next) {
exports.restrict = function (req, res, next) {
if (req.session.user) {
next();
} else {
req.session.error = 'Access denied!';
const error = 'Access denied! (Login required)';
req.session.error = error;
res.redirect('/login?redirect=' + encodeURIComponent(req.originalUrl));
}
}

/** @type {import('express').RequestHandler} */
exports.restrictNetwork = function (req, res, next) {
const { nwid } = req.params;
if (hasRightsForNetwork(req.session.user, nwid)) {
next();
} else {
const error = 'Access denied! (No permission to access this network)';
res.render('error', { error });
}
}

exports.restrictSuperAdmin = function (req, res, next) {
if (isSuperAdmin(req.session.user)) {
next();
} else {
const error = 'Access denied! (Super admin required)';
res.render('error', { error });
}
}

// OpenID Connect (OIDC) Auth:

const { Issuer, generators } = require('openid-client')

const {
BASE_URL,
AUTH_OIDC_ISSUER,
AUTH_OIDC_CLIENT_ID,
AUTH_OIDC_CLIENT_SECRET,
AUTH_OIDC_ONLY,
} = process.env;

const useOidc = !!AUTH_OIDC_ISSUER;
const callbackUrl = BASE_URL + '/login_oidc_cb';

exports.oidcAvailable = useOidc;
exports.oidcOnly = AUTH_OIDC_ONLY === 'true';

const oidcClient = !useOidc ? null : (async () => {
const issuer = await Issuer.discover(AUTH_OIDC_ISSUER);
const client = new issuer.Client({
client_id: AUTH_OIDC_CLIENT_ID,
client_secret: AUTH_OIDC_CLIENT_SECRET,
redirect_uris: [callbackUrl],
response_types: ['code'],
});
return client;
})()

/** @type {import('express').RequestHandler} */
exports.oidcLogin = async (req, res) => {
// TODO
const client = await oidcClient;
const nonce = generators.nonce();
const url = client.authorizationUrl({
scope: 'openid email profile',
response_mode: 'form_post',
nonce,
});
req.session.nonce = nonce;
res.redirect(url);
};


/** @type {import('express').RequestHandler} */
exports.oidcCallback = async (req, res) => {
try {
const client = await oidcClient;
const { nonce } = req.session;
const params = client.callbackParams(req);
const tokenSet = await client.callback(callbackUrl, params, { nonce });
const claims = tokenSet.claims();
console.log('validated ID Token claims %j', claims);
const user = userFromClaims(claims);
if (user.role != 'superadmin' && user.networks.length === 0) {
res.status(500).render('error', { error: 'user has no permission' });
return;
}
req.session.user = user;
res.redirect('/controller');
} catch (error) {
console.error(error);
res.status(500).render('error', { error: 'oidc error' });
}
};

/**
* @param {import('openid-client').IdTokenClaims} claims
*/
function userFromClaims(claims) {
const name = claims.email || claims.preferred_username;
const roles = claims.ztncui_roles;
let role = 'admin';
const networks = [];
for (const r of roles) {
const [type, arg1, ...rest] = r.split('-');
if (type == 'role') {
role = arg1;
} else if (type == 'nwid') {
networks.push(...rest);
} else {
throw new Error('unknown role ' + r);
}
}
return { name, role, networks }
}
Loading