diff --git a/.gitignore b/.gitignore index 5fd8ac473c828bc433648b6f78402c2eab4d0583..af6004a96748710f3c51d22fdd2046c32db86500 100644 --- a/.gitignore +++ b/.gitignore @@ -15,4 +15,5 @@ local/ target/ *.swp *.vscode +.vscode/ nomad.yaml diff --git a/gui/package.json b/gui/package.json index d0aa28092615accb7dc89952873aae70f396d5f7..c377ed0865f75ae7845e2f412442126d2bc68eb9 100644 --- a/gui/package.json +++ b/gui/package.json @@ -13,6 +13,7 @@ "fetch": "^1.1.0", "file-saver": "^2.0.0", "html-to-react": "^1.3.3", + "keycloak-js": "^6.0.0", "marked": "^0.6.0", "material-ui-chip-input": "^1.0.0-beta.14", "material-ui-flat-pagination": "^3.2.0", @@ -28,6 +29,7 @@ "react-dropzone": "^5.0.1", "react-highlight": "^0.12.0", "react-json-view": "^1.19.1", + "react-keycloak": "^6.1.0", "react-router-dom": "^4.3.1", "react-router-hash-link": "^1.2.0", "react-scripts": "1.1.4", diff --git a/gui/src/components/LoginLogout.js b/gui/src/components/LoginLogout.js index 0d474e2cbb2259abcaa4d3805cd3c1f6024b83dd..297c79eb311b46d94c50b2f7af94d0c36e5f1bf3 100644 --- a/gui/src/components/LoginLogout.js +++ b/gui/src/components/LoginLogout.js @@ -6,6 +6,7 @@ import { compose } from 'recompose' import { Button, DialogTitle, DialogContent, DialogContentText, TextField, DialogActions, Dialog, FormGroup } from '@material-ui/core' import { withApi } from './api' +import { withKeycloak } from 'react-keycloak' class LoginLogout extends React.Component { static propTypes = { @@ -18,7 +19,8 @@ class LoginLogout extends React.Component { variant: PropTypes.string, color: PropTypes.string, onLoggedIn: PropTypes.func, - onLoggedOut: PropTypes.func + onLoggedOut: PropTypes.func, + keycloak: PropTypes.object.isRequired } static styles = theme => ({ @@ -102,7 +104,13 @@ class LoginLogout extends React.Component { } render() { - const { classes, user, variant, color, isLoggingIn } = this.props + const { classes, variant, color, isLoggingIn, keycloak } = this.props + + let user = null + if (keycloak.authenticated) { + user = {} + } + const { failure } = this.state if (user) { return ( @@ -113,7 +121,7 @@ class LoginLogout extends React.Component { <Button className={classes.button} variant={variant} color={color} - onClick={this.handleLogout} + onClick={() => keycloak.logout()} >Logout</Button> </div> ) @@ -121,8 +129,7 @@ class LoginLogout extends React.Component { return ( <div className={classes.root}> <Button - className={isLoggingIn ? classes.buttonDisabled : classes.button} variant={variant} color={color} disabled={isLoggingIn} - onClick={() => this.setState({loginDialogOpen: true})} + className={classes.button} variant={variant} color={color} onClick={() => keycloak.login()} >Login</Button> <Dialog disableBackdropClick disableEscapeKeyDown @@ -184,4 +191,4 @@ class LoginLogout extends React.Component { } } -export default compose(withApi(false), withStyles(LoginLogout.styles))(LoginLogout) +export default compose(withKeycloak, withApi(false), withStyles(LoginLogout.styles))(LoginLogout) diff --git a/gui/src/index.js b/gui/src/index.js index 2307e8d377cae0df9d643f3dfa3df5f9f8abd21a..fb221b09f9a107702d3893da39e8c489b9006132 100644 --- a/gui/src/index.js +++ b/gui/src/index.js @@ -9,14 +9,24 @@ import { Router } from 'react-router-dom' import history from './history' import PiwikReactRouter from 'piwik-react-router' import { sendTrackingData, matomoUrl, matomoSiteId } from './config' +import Keycloak from 'keycloak-js' +import { KeycloakProvider } from 'react-keycloak' const matomo = sendTrackingData ? PiwikReactRouter({ url: matomoUrl, siteId: matomoSiteId }) : null +const keycloak = Keycloak({ + url: 'http://localhost:8002/auth', + realm: 'fairdi_nomad_test', + clientId: 'nomad_gui_dev' +}) + ReactDOM.render( - <Router history={sendTrackingData ? matomo.connectToHistory(history) : history}> - <App /> - </Router>, document.getElementById('root')) + <KeycloakProvider keycloak={keycloak} initConfig={{onLoad: 'check-sso'}} > + <Router history={sendTrackingData ? matomo.connectToHistory(history) : history}> + <App /> + </Router> + </KeycloakProvider>, document.getElementById('root')) registerServiceWorker() diff --git a/gui/yarn.lock b/gui/yarn.lock index 03eefe91e86ce86abde66f4a97ebfde4914d52ea..397d77d324dfc18ee5ff50451bb614ac6e3e17b1 100644 --- a/gui/yarn.lock +++ b/gui/yarn.lock @@ -3922,7 +3922,7 @@ hoist-non-react-statics@^2.3.1, hoist-non-react-statics@^2.5.0: version "2.5.5" resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-2.5.5.tgz#c5903cf409c0dfd908f388e619d86b9c1174cb47" -hoist-non-react-statics@^3.0.0: +hoist-non-react-statics@^3.0.0, hoist-non-react-statics@^3.3.0: version "3.3.0" resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.0.tgz#b09178f0122184fb95acf525daaecb4d8f45958b" dependencies: @@ -4985,6 +4985,14 @@ jsx-ast-utils@^2.0.0, jsx-ast-utils@^2.0.1: dependencies: array-includes "^3.0.3" +keycloak-js@^6.0.0: + version "6.0.1" + resolved "https://registry.yarnpkg.com/keycloak-js/-/keycloak-js-6.0.1.tgz#329a5e77210dfc4a7d4acf96f95dd0132455bea3" + +keycloak@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/keycloak/-/keycloak-1.2.0.tgz#2ff4cc57102842f2eecc2f4bb206306596d7b025" + keycode@^2.1.7, keycode@^2.1.9: version "2.2.0" resolved "https://registry.yarnpkg.com/keycode/-/keycode-2.2.0.tgz#3d0af56dc7b8b8e5cba8d0a97f107204eec22b04" @@ -6380,6 +6388,14 @@ prop-types@^15.5.10, prop-types@^15.5.4, prop-types@^15.5.8, prop-types@^15.6.0, loose-envify "^1.3.1" object-assign "^4.1.1" +prop-types@^15.7.2: + version "15.7.2" + resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.7.2.tgz#52c41e75b8c87e72b9d9360e0206b99dcbffa6c5" + dependencies: + loose-envify "^1.4.0" + object-assign "^4.1.1" + react-is "^16.8.1" + proxy-addr@~2.0.4: version "2.0.4" resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.4.tgz#ecfc733bf22ff8c6f407fa275327b9ab67e48b93" @@ -6632,6 +6648,10 @@ react-is@^16.3.2, react-is@^16.6.3, react-is@^16.7.0: version "16.7.0" resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.7.0.tgz#c1bd21c64f1f1364c6f70695ec02d69392f41bfa" +react-is@^16.8.1: + version "16.9.0" + resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.9.0.tgz#21ca9561399aad0ff1a7701c01683e8ca981edcb" + react-json-view@^1.19.1: version "1.19.1" resolved "https://registry.yarnpkg.com/react-json-view/-/react-json-view-1.19.1.tgz#95d8e59e024f08a25e5dc8f076ae304eed97cf5c" @@ -6641,6 +6661,13 @@ react-json-view@^1.19.1: react-lifecycles-compat "^3.0.4" react-textarea-autosize "^6.1.0" +react-keycloak@^6.1.0: + version "6.1.0" + resolved "https://registry.yarnpkg.com/react-keycloak/-/react-keycloak-6.1.0.tgz#672eed832de231e981a717413dc10286dbe701f2" + dependencies: + hoist-non-react-statics "^3.3.0" + prop-types "^15.7.2" + react-lifecycles-compat@^3.0.2, react-lifecycles-compat@^3.0.4: version "3.0.4" resolved "https://registry.yarnpkg.com/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz#4f1a273afdfc8f3488a8c516bfda78f872352362" diff --git a/nomad/config.py b/nomad/config.py index 3a6dc047ddc780a10fe64ea77e7476dfcdb9218d..daa6808c76152f5caab89032f0fbd7fc7d4f22e6 100644 --- a/nomad/config.py +++ b/nomad/config.py @@ -124,6 +124,15 @@ repository_db = NomadConfig( mode='fairdi' ) +keycloak = NomadConfig( + server_url='http://localhost:8002/auth/', + realm_name='fairdi_nomad_test', + username='admin', + password='password', + client_id='nomad_api', + client_secret_key='0f9ec82f-a1dc-4405-a80e-593160aeea42' +) + mongo = NomadConfig( host='localhost', port=27017, diff --git a/nomad/infrastructure.py b/nomad/infrastructure.py index b6bc92ec4f70643c54e1e4f82b5b2be11baf2f4c..3008788359a1b3f8a0964e4ff5d5db0fa3d2c360 100644 --- a/nomad/infrastructure.py +++ b/nomad/infrastructure.py @@ -32,6 +32,7 @@ from mongoengine import connect from passlib.hash import bcrypt import smtplib from email.mime.text import MIMEText +import keycloak from nomad import config, utils @@ -48,6 +49,11 @@ repository_db = None repository_db_conn = None """ The repository postgres db sqlalchemy connection. """ +keycloak_oidc_client = None +""" The keycode OpenID connect client. """ +keycloak_admin_client = None +""" The keycode admin client. """ + def setup(): """ @@ -108,6 +114,26 @@ def setup_elastic(): return elastic_client +def setup_keycloak(): + """ Creates a keycloak client. """ + global keycloak_oidc_client + global keycloak_admin_client + + keycloak_oidc_client = keycloak.KeycloakOpenID( + server_url=config.keycloak.server_url, + client_id=config.keycloak.client_id, + realm_name=config.keycloak.realm_name, + client_secret_key=config.keycloak.client_secret_key) + + keycloak_admin_client = keycloak.KeycloakAdmin( + server_url=config.keycloak.server_url, + username=config.keycloak.username, + password=config.keycloak.password, + realm_name='master', + verify=True) + keycloak_admin_client.realm_name = config.keycloak.realm_name + + def setup_repository_db(**kwargs): """ Creates a connection and stores it in the module variables. """ repo_args = dict(readonly=False) @@ -424,12 +450,12 @@ def send_mail(name: str, email: str, message: str, subject: str): msg = MIMEText(message) msg['Subject'] = subject - msg['From'] = 'The nomad team <%s>' % config.mail.from_address + msg['From'] = 'The NOMAD team <%s>' % config.mail.from_address msg['To'] = name to_addrs = [email] if config.mail.cc_address is not None: - msg['Cc'] = 'The nomad team <%s>' % config.mail.cc_address + msg['Cc'] = 'The NOMAD team <%s>' % config.mail.cc_address to_addrs.append(config.mail.cc_address) try: @@ -438,3 +464,21 @@ def send_mail(name: str, email: str, message: str, subject: str): logger.error('Could not send email', exc_info=e) server.quit() + + +if __name__ == '__main__': + import logging, time + + config.console_log_level = logging.DEBUG + setup_logging() + setup_keycloak() + + token = keycloak_oidc_client.token( + username='sheldon.cooper@nomad-coe.eu', password='password') + + while True: + print(keycloak_oidc_client.userinfo(token['access_token'])) + keycloak_user_id = keycloak_admin_client.get_user_id('sheldon.cooper@nomad-coe.eu') + print(keycloak_admin_client.get_user(keycloak_user_id)) + time.sleep(5) + diff --git a/nomad/utils.py b/nomad/utils.py index e6a0a353c2f818b8d6989922d7fb55ac22c05143..36739ebc4f1f68e1bedd82b0504798037f04c29f 100644 --- a/nomad/utils.py +++ b/nomad/utils.py @@ -112,7 +112,10 @@ class LogstashHandler(logstash.TCPLogstashHandler): def filter(self, record): if super().filter(record): - is_structlog = record.msg.startswith('{') and record.msg.endswith('}') + is_structlog = False + if isinstance(record.msg, str): + is_structlog = record.msg.startswith('{') and record.msg.endswith('}') + if is_structlog: return True else: diff --git a/ops/docker-compose/nomad/docker-compose.override.yml b/ops/docker-compose/nomad/docker-compose.override.yml index 3782b1c46087fdf831e3bdd20e06f610d895f200..c0e4c804b862c24250230ee108a93603602c682c 100644 --- a/ops/docker-compose/nomad/docker-compose.override.yml +++ b/ops/docker-compose/nomad/docker-compose.override.yml @@ -50,12 +50,6 @@ services: ports: - 8000:8000 - # NOMAD-coe search api - coeapi: - restart: 'no' - ports: - - 8111:8111 - # nomad gui gui: restart: 'no' diff --git a/ops/docker-compose/nomad/docker-compose.prod.yml b/ops/docker-compose/nomad/docker-compose.prod.yml index 21d50e5428cf64e2ac9e1c3650760bee3ea0c897..d317087e25d7051738679845e56b24209f75c4d2 100644 --- a/ops/docker-compose/nomad/docker-compose.prod.yml +++ b/ops/docker-compose/nomad/docker-compose.prod.yml @@ -15,6 +15,10 @@ version: '3.4' services: + # keycloak for user management + keycloak: + volumes: + - /nomad/fairdi/db/keycloak:/opt/jboss/keycloak/standalone postgres: ports: diff --git a/ops/docker-compose/nomad/docker-compose.yml b/ops/docker-compose/nomad/docker-compose.yml index 93cd8586648bed2e6c168463544a0f165c31ef4c..4c26aeb691ed9c320f99bf81df6d3ae37c3c667f 100644 --- a/ops/docker-compose/nomad/docker-compose.yml +++ b/ops/docker-compose/nomad/docker-compose.yml @@ -19,6 +19,7 @@ x-common-variables: &nomad_backend_env NOMAD_LOGSTASH_HOST: elk NOMAD_ELASTIC_HOST: elastic NOMAD_MONGO_HOST: mongo + NOMAD_KEYCLOAK_HOST: keycloak services: # postgres for NOMAD-coe repository API and GUI @@ -32,6 +33,20 @@ services: volumes: - nomad_postgres:/var/lib/postgresql/data + # keycload for user management + keycloak: + restart: always + image: jboss/keycloak + container_name: nomad_keycloak + environment: + DB_VENDOR: "h2" + KEYCLOAK_USER: "admin" + KEYCLOAK_PASSWORD: "password" + volumes: + - nomad_keycloak:/opt/jboss/keycloak/standalone + ports: + - 8002:8080 + # broker for celery rabbitmq: restart: always @@ -74,6 +89,7 @@ services: <<: *nomad_backend_env NOMAD_SERVICE: nomad_worker links: + - keycloak - postgres - rabbitmq - elastic @@ -95,6 +111,7 @@ services: NOMAD_SERVICES_API_SECRET: ${API_SECRET} NOMAD_SERVICE: nomad_api links: + - keycloak - postgres - rabbitmq - elastic @@ -103,18 +120,6 @@ services: - ${VOLUME_BINDS}/fs:/app/.volumes/fs command: python -m gunicorn.app.wsgiapp -w 4 --log-config ops/gunicorn.log.conf -b 0.0.0.0:8000 --timeout 300 nomad.api:app - # NOMAD-coe search api - coeapi: - restart: always - image: gitlab-registry.mpcdf.mpg.de/nomad-lab/nomad-fair/coe-repowebservice:latest - container_name: nomad_coeapi - environment: - REPO_DB_JDBC_URL: jdbc:postgresql://postgres:5432/nomad - REPO_ELASTIC_URL: elasticsearch://elastic:9200 - links: - - postgres - - elastic - # nomad gui gui: restart: always @@ -137,6 +142,7 @@ services: command: nginx -g 'daemon off;' volumes: + nomad_keycloak: nomad_postgres: nomad_mongo: nomad_elastic: diff --git a/requirements.txt b/requirements.txt index de549f4096cdbe5dd34f640031ca3ec0eae7f4f1..9f6bfb7446aac42bb9ac44e1c081fe6615a30b1a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -48,6 +48,7 @@ pyyaml tabulate cachetools zipfile37 +python-keycloak # dev/ops related setuptools