diff --git a/.github/workflows/pr-sso-tests.yml b/.github/workflows/pr-sso-tests.yml new file mode 100644 index 00000000000..197ffc62166 --- /dev/null +++ b/.github/workflows/pr-sso-tests.yml @@ -0,0 +1,39 @@ +name: 'Browser PR single sign on tests' + +on: + pull_request: + branches: [master] + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number }} + cancel-in-progress: true + +jobs: + e2e-tests: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-node@v3 + with: + node-version-file: '.nvmrc' + - run: npm -g install yarn serve + - run: yarn install --frozen-lockfile + - run: yarn build + - run: sudo apt-get update + - run: sudo apt-get -y install libgtk2.0-0 libgtk-3-0 libgbm-dev libnotify-dev libgconf-2-4 libnss3 libxss1 libasound2 libxtst6 xauth xvfb + - name: Setup Keycloak and Neo4j + working-directory: ./docker/sso-keycloak + run: docker-compose -f docker-compose.yml up -d --remove-orphans + - name: Wait for Keycloak and Neo4j config containers to finish + run: sleep 90 + - run: npx serve -l 8080 dist & npm run wait-on-neo4j && yarn wait-on-dev + - run: echo "Servers ready!" + - run: yarn e2e-sso + - name: Upload test screenshots + if: failure() + uses: actions/upload-artifact@v3 + with: + name: test-screenshots-sso + path: | + ./e2e_tests/screenshots + ./e2e_tests/videos diff --git a/docker/sso-keycloak/.env b/docker/sso-keycloak/.env new file mode 100644 index 00000000000..1be4b1de1ef --- /dev/null +++ b/docker/sso-keycloak/.env @@ -0,0 +1,6 @@ +NEO4J_VERSION=4.4.9-enterprise +NEO4J_PASSWORD=password +KEYCLOAK_VERSION=19.0.1 +KEYCLOAK_ADMIN_USER=admin +KEYCLOAK_ADMIN_PASSWORD=password +KEYCLOAK_CONFIG_VERSION=latest-19.0.3 diff --git a/docker/sso-keycloak/docker-compose.yml b/docker/sso-keycloak/docker-compose.yml new file mode 100644 index 00000000000..a3a03bddef9 --- /dev/null +++ b/docker/sso-keycloak/docker-compose.yml @@ -0,0 +1,81 @@ +version: '3.7' +services: + neo4j: + image: neo4j:${NEO4J_VERSION} + ports: + - '7474:7474' + - '7687:7687' + environment: + - NEO4J_ACCEPT_LICENSE_AGREEMENT=yes + - NEO4J_AUTH=neo4j/${NEO4J_PASSWORD:-password} + - NEO4J_dbms_connector_bolt_advertised__address=0.0.0.0:7687 + - NEO4J_apoc_trigger_enabled=true + - NEO4J_dbms_security_authentication__providers=oidc-keycloak,native + - NEO4J_dbms_security_authorization__providers=oidc-keycloak,native + - NEO4J_dbms_security_oidc_keycloak_display__name=Keycloak + - NEO4J_dbms_security_oidc_keycloak_.auth__flow=pkce + - NEO4J_dbms_security_oidc_keycloak_auth__endpoint=http://127.0.0.1:8180/realms/my-realm/protocol/openid-connect/auth + - NEO4J_dbms_security_oidc_keycloak_token__endpoint=http://127.0.0.1:8180/realms/my-realm/protocol/openid-connect/token + - NEO4J_dbms_security_oidc_keycloak_jwks__uri=http://keycloak:8180/realms/my-realm/protocol/openid-connect/certs + - NEO4J_dbms_security_oidc_keycloak_params=client_id=neo4j-sso;response_type=code;scope=openid email roles + - NEO4J_dbms_security_oidc_keycloak_audience=account + - NEO4J_dbms_security_oidc_keycloak_issuer=http://127.0.0.1:8180/realms/my-realm + - NEO4J_dbms_security_oidc_keycloak_client__id=neo4j-sso + - NEO4J_dbms_security_oidc_keycloak_claims_username=preferred_username + - NEO4J_dbms_security_oidc_keycloak_claims_groups=roles + - NEO4J_browser_allow__outgoing__connections=false + volumes: + - 'neo4j_data:/data' + neo4j-config-cli: + image: graphaware/neo4j-config-cli:2.5.0 + environment: + - NEO4J_USER=neo4j + - NEO4J_PASSWORD=password + - NEO4J_URI=bolt://neo4j:7687 + - IMPORT_PATH=/config + volumes: + - './resources/neo4j-config:/config' + keycloak: + image: quay.io/keycloak/keycloak:${KEYCLOAK_VERSION} + environment: + - KEYCLOAK_ADMIN=${KEYCLOAK_ADMIN_USER} + - KEYCLOAK_ADMIN_PASSWORD=${KEYCLOAK_ADMIN_PASSWORD} + - KC_DB_URL=jdbc:postgresql://postgres/keycloak + - KC_DB_USERNAME=keycloak + - KC_DB_PASSWORD=password + - KC_HTTP_ENBALED=true + - KC_HTTP_PORT=8180 + - KC_HOSTNAME=127.0.0.1 + - KC_HOSTNAME_PORT=8180 + - KC_HOSTNAME_ADMIN=localhost + command: + - start-dev --db=postgres + ports: + - '8180:8180' + depends_on: + - postgres + postgres: + image: postgres:15-alpine + environment: + POSTGRES_DB: keycloak + POSTGRES_USER: keycloak + POSTGRES_PASSWORD: password + command: -p 5432 + expose: + - '5433' # Publishes 5433 to other containers but NOT to host machine + volumes: + - 'keycloak_data:/var/lib/postgresql@15/data' + keycloak-config: + image: adorsys/keycloak-config-cli:${KEYCLOAK_CONFIG_VERSION} + environment: + - KEYCLOAK_URL=http://keycloak:8180 + - KEYCLOAK_USER=admin + - KEYCLOAK_PASSWORD=password + - KEYCLOAK_AVAILABILITYCHECK_ENABLED=true + - KEYCLOAK_AVAILABILITYCHECK_TIMEOUT=90s + - IMPORT_PATH=/config + volumes: + - ./resources/keycloak-config:/config +volumes: + neo4j_data: + keycloak_data: diff --git a/docker/sso-keycloak/resources/keycloak-config/config.json b/docker/sso-keycloak/resources/keycloak-config/config.json new file mode 100644 index 00000000000..d9e65e54ac9 --- /dev/null +++ b/docker/sso-keycloak/resources/keycloak-config/config.json @@ -0,0 +1,141 @@ +{ + "enabled": true, + "realm": "my-realm", + "users": [ + { + "username": "admin", + "email": "dave.lister@example.com", + "enabled": true, + "credentials": [ + { + "type": "password", + "value": "password" + } + ], + "clientRoles": { + "neo4j-sso": ["admin"] + } + }, + { + "username": "analyst", + "email": "analyst@example.com", + "enabled": true, + "credentials": [ + { + "type": "password", + "value": "password" + } + ], + "clientRoles": { + "neo4j-sso": ["analyst"] + } + } + ], + "roles": { + "realm": [], + "client": { + "neo4j-sso": [ + { + "name": "admin", + "description": "neo4j admin role", + "composite": false, + "clientRole": true, + "attributes": {} + }, + { + "name": "analyst", + "description": "neo4j analyst role", + "composite": false, + "clientRole": true, + "attributes": {} + } + ] + } + }, + "clients": [ + { + "clientId": "neo4j-sso", + "surrogateAuthRequired": false, + "enabled": true, + "alwaysDisplayInConsole": false, + "clientAuthenticatorType": "client-secret", + "redirectUris": ["*"], + "webOrigins": ["*"], + "notBefore": 0, + "bearerOnly": false, + "consentRequired": false, + "standardFlowEnabled": true, + "implicitFlowEnabled": true, + "directAccessGrantsEnabled": true, + "serviceAccountsEnabled": false, + "publicClient": true, + "frontchannelLogout": false, + "protocol": "openid-connect", + "attributes": { + "saml.assertion.signature": "false", + "id.token.as.detached.signature": "false", + "saml.multivalued.roles": "false", + "saml.force.post.binding": "false", + "saml.encrypt": "false", + "oauth2.device.authorization.grant.enabled": "true", + "backchannel.logout.revoke.offline.tokens": "false", + "saml.server.signature": "false", + "saml.server.signature.keyinfo.ext": "false", + "use.refresh.tokens": "true", + "exclude.session.state.from.auth.response": "false", + "oidc.ciba.grant.enabled": "false", + "saml.artifact.binding": "false", + "backchannel.logout.session.required": "true", + "client_credentials.use_refresh_token": "false", + "saml_force_name_id_format": "false", + "saml.client.signature": "false", + "tls.client.certificate.bound.access.tokens": "false", + "require.pushed.authorization.requests": "false", + "saml.authnstatement": "false", + "display.on.consent.screen": "false", + "saml.onetimeuse.condition": "false" + }, + "authenticationFlowBindingOverrides": {}, + "fullScopeAllowed": true, + "nodeReRegistrationTimeout": -1, + "protocolMappers": [ + { + "name": "client-roles-to-roles-claim", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-client-role-mapper", + "consentRequired": false, + "config": { + "multivalued": "true", + "userinfo.token.claim": "true", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "roles", + "jsonType.label": "String", + "usermodel.clientRoleMapping.clientId": "neo4j-sso" + } + }, + { + "name": "username", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-property-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "username", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "email", + "jsonType.label": "String" + } + } + ], + "defaultClientScopes": ["web-origins", "roles", "profile", "email"], + "optionalClientScopes": [ + "address", + "phone", + "offline_access", + "microprofile-jwt" + ] + } + ] +} diff --git a/docker/sso-keycloak/resources/neo4j-config/admin-db.json b/docker/sso-keycloak/resources/neo4j-config/admin-db.json new file mode 100644 index 00000000000..b6c77a8a1cc --- /dev/null +++ b/docker/sso-keycloak/resources/neo4j-config/admin-db.json @@ -0,0 +1,16 @@ +{ + "kind": "Database", + "name": "admins", + "dropIfExists": true, + "skipCreate": false, + "indexes": { + "fulltext": [ + { + "labels": ["Person"], + "properties": ["name"], + "name": "Person" + } + ] + }, + "seeds": ["admins.cypher"] +} diff --git a/docker/sso-keycloak/resources/neo4j-config/admins.cypher b/docker/sso-keycloak/resources/neo4j-config/admins.cypher new file mode 100644 index 00000000000..8a7b7e98e3a --- /dev/null +++ b/docker/sso-keycloak/resources/neo4j-config/admins.cypher @@ -0,0 +1 @@ +CREATE (n:Admin {name: "Dave Lister"}); diff --git a/docker/sso-keycloak/resources/neo4j-config/movies-db.json b/docker/sso-keycloak/resources/neo4j-config/movies-db.json new file mode 100644 index 00000000000..c66279051d8 --- /dev/null +++ b/docker/sso-keycloak/resources/neo4j-config/movies-db.json @@ -0,0 +1,24 @@ +{ + "kind": "Database", + "name": "movies", + "dropIfExists": true, + "skipCreate": false, + "indexes": { + "fulltext": [ + { + "labels": ["Person"], + "properties": ["name"], + "name": "Person" + } + ] + }, + "constraints": { + "unique": [ + { + "label": "Movie", + "property": "title" + } + ] + }, + "seeds": ["movies.cypher"] +} diff --git a/docker/sso-keycloak/resources/neo4j-config/movies.cypher b/docker/sso-keycloak/resources/neo4j-config/movies.cypher new file mode 100644 index 00000000000..70239323ccc --- /dev/null +++ b/docker/sso-keycloak/resources/neo4j-config/movies.cypher @@ -0,0 +1,505 @@ +CREATE (TheMatrix:Movie {title:'The Matrix', released:1999, tagline:'Welcome to the Real World'}) +CREATE (Keanu:Person {name:'Keanu Reeves', born:1964}) +CREATE (Carrie:Person {name:'Carrie-Anne Moss', born:1967}) +CREATE (Laurence:Person {name:'Laurence Fishburne', born:1961}) +CREATE (Hugo:Person {name:'Hugo Weaving', born:1960}) +CREATE (LillyW:Person {name:'Lilly Wachowski', born:1967}) +CREATE (LanaW:Person {name:'Lana Wachowski', born:1965}) +CREATE (JoelS:Person {name:'Joel Silver', born:1952}) +CREATE +(Keanu)-[:ACTED_IN {roles:['Neo']}]->(TheMatrix), +(Carrie)-[:ACTED_IN {roles:['Trinity']}]->(TheMatrix), +(Laurence)-[:ACTED_IN {roles:['Morpheus']}]->(TheMatrix), +(Hugo)-[:ACTED_IN {roles:['Agent Smith']}]->(TheMatrix), +(LillyW)-[:DIRECTED]->(TheMatrix), +(LanaW)-[:DIRECTED]->(TheMatrix), +(JoelS)-[:PRODUCED]->(TheMatrix) + +CREATE (Emil:Person {name:"Emil Eifrem", born:1978}) +CREATE (Emil)-[:ACTED_IN {roles:["Emil"]}]->(TheMatrix) + +CREATE (TheMatrixReloaded:Movie {title:'The Matrix Reloaded', released:2003, tagline:'Free your mind'}) +CREATE +(Keanu)-[:ACTED_IN {roles:['Neo']}]->(TheMatrixReloaded), +(Carrie)-[:ACTED_IN {roles:['Trinity']}]->(TheMatrixReloaded), +(Laurence)-[:ACTED_IN {roles:['Morpheus']}]->(TheMatrixReloaded), +(Hugo)-[:ACTED_IN {roles:['Agent Smith']}]->(TheMatrixReloaded), +(LillyW)-[:DIRECTED]->(TheMatrixReloaded), +(LanaW)-[:DIRECTED]->(TheMatrixReloaded), +(JoelS)-[:PRODUCED]->(TheMatrixReloaded) + +CREATE (TheMatrixRevolutions:Movie {title:'The Matrix Revolutions', released:2003, tagline:'Everything that has a beginning has an end'}) +CREATE +(Keanu)-[:ACTED_IN {roles:['Neo']}]->(TheMatrixRevolutions), +(Carrie)-[:ACTED_IN {roles:['Trinity']}]->(TheMatrixRevolutions), +(Laurence)-[:ACTED_IN {roles:['Morpheus']}]->(TheMatrixRevolutions), +(Hugo)-[:ACTED_IN {roles:['Agent Smith']}]->(TheMatrixRevolutions), +(LillyW)-[:DIRECTED]->(TheMatrixRevolutions), +(LanaW)-[:DIRECTED]->(TheMatrixRevolutions), +(JoelS)-[:PRODUCED]->(TheMatrixRevolutions) + +CREATE (TheDevilsAdvocate:Movie {title:"The Devil's Advocate", released:1997, tagline:'Evil has its winning ways'}) +CREATE (Charlize:Person {name:'Charlize Theron', born:1975}) +CREATE (Al:Person {name:'Al Pacino', born:1940}) +CREATE (Taylor:Person {name:'Taylor Hackford', born:1944}) +CREATE +(Keanu)-[:ACTED_IN {roles:['Kevin Lomax']}]->(TheDevilsAdvocate), +(Charlize)-[:ACTED_IN {roles:['Mary Ann Lomax']}]->(TheDevilsAdvocate), +(Al)-[:ACTED_IN {roles:['John Milton']}]->(TheDevilsAdvocate), +(Taylor)-[:DIRECTED]->(TheDevilsAdvocate) + +CREATE (AFewGoodMen:Movie {title:"A Few Good Men", released:1992, tagline:"In the heart of the nation's capital, in a courthouse of the U.S. government, one man will stop at nothing to keep his honor, and one will stop at nothing to find the truth."}) +CREATE (TomC:Person {name:'Tom Cruise', born:1962}) +CREATE (JackN:Person {name:'Jack Nicholson', born:1937}) +CREATE (DemiM:Person {name:'Demi Moore', born:1962}) +CREATE (KevinB:Person {name:'Kevin Bacon', born:1958}) +CREATE (KieferS:Person {name:'Kiefer Sutherland', born:1966}) +CREATE (NoahW:Person {name:'Noah Wyle', born:1971}) +CREATE (CubaG:Person {name:'Cuba Gooding Jr.', born:1968}) +CREATE (KevinP:Person {name:'Kevin Pollak', born:1957}) +CREATE (JTW:Person {name:'J.T. Walsh', born:1943}) +CREATE (JamesM:Person {name:'James Marshall', born:1967}) +CREATE (ChristopherG:Person {name:'Christopher Guest', born:1948}) +CREATE (RobR:Person {name:'Rob Reiner', born:1947}) +CREATE (AaronS:Person {name:'Aaron Sorkin', born:1961}) +CREATE +(TomC)-[:ACTED_IN {roles:['Lt. Daniel Kaffee']}]->(AFewGoodMen), +(JackN)-[:ACTED_IN {roles:['Col. Nathan R. Jessup']}]->(AFewGoodMen), +(DemiM)-[:ACTED_IN {roles:['Lt. Cdr. JoAnne Galloway']}]->(AFewGoodMen), +(KevinB)-[:ACTED_IN {roles:['Capt. Jack Ross']}]->(AFewGoodMen), +(KieferS)-[:ACTED_IN {roles:['Lt. Jonathan Kendrick']}]->(AFewGoodMen), +(NoahW)-[:ACTED_IN {roles:['Cpl. Jeffrey Barnes']}]->(AFewGoodMen), +(CubaG)-[:ACTED_IN {roles:['Cpl. Carl Hammaker']}]->(AFewGoodMen), +(KevinP)-[:ACTED_IN {roles:['Lt. Sam Weinberg']}]->(AFewGoodMen), +(JTW)-[:ACTED_IN {roles:['Lt. Col. Matthew Andrew Markinson']}]->(AFewGoodMen), +(JamesM)-[:ACTED_IN {roles:['Pfc. Louden Downey']}]->(AFewGoodMen), +(ChristopherG)-[:ACTED_IN {roles:['Dr. Stone']}]->(AFewGoodMen), +(AaronS)-[:ACTED_IN {roles:['Man in Bar']}]->(AFewGoodMen), +(RobR)-[:DIRECTED]->(AFewGoodMen), +(AaronS)-[:WROTE]->(AFewGoodMen) + +CREATE (TopGun:Movie {title:"Top Gun", released:1986, tagline:'I feel the need, the need for speed.'}) +CREATE (KellyM:Person {name:'Kelly McGillis', born:1957}) +CREATE (ValK:Person {name:'Val Kilmer', born:1959}) +CREATE (AnthonyE:Person {name:'Anthony Edwards', born:1962}) +CREATE (TomS:Person {name:'Tom Skerritt', born:1933}) +CREATE (MegR:Person {name:'Meg Ryan', born:1961}) +CREATE (TonyS:Person {name:'Tony Scott', born:1944}) +CREATE (JimC:Person {name:'Jim Cash', born:1941}) +CREATE +(TomC)-[:ACTED_IN {roles:['Maverick']}]->(TopGun), +(KellyM)-[:ACTED_IN {roles:['Charlie']}]->(TopGun), +(ValK)-[:ACTED_IN {roles:['Iceman']}]->(TopGun), +(AnthonyE)-[:ACTED_IN {roles:['Goose']}]->(TopGun), +(TomS)-[:ACTED_IN {roles:['Viper']}]->(TopGun), +(MegR)-[:ACTED_IN {roles:['Carole']}]->(TopGun), +(TonyS)-[:DIRECTED]->(TopGun), +(JimC)-[:WROTE]->(TopGun) + +CREATE (JerryMaguire:Movie {title:'Jerry Maguire', released:2000, tagline:'The rest of his life begins now.'}) +CREATE (ReneeZ:Person {name:'Renee Zellweger', born:1969}) +CREATE (KellyP:Person {name:'Kelly Preston', born:1962}) +CREATE (JerryO:Person {name:"Jerry O'Connell", born:1974}) +CREATE (JayM:Person {name:'Jay Mohr', born:1970}) +CREATE (BonnieH:Person {name:'Bonnie Hunt', born:1961}) +CREATE (ReginaK:Person {name:'Regina King', born:1971}) +CREATE (JonathanL:Person {name:'Jonathan Lipnicki', born:1996}) +CREATE (CameronC:Person {name:'Cameron Crowe', born:1957}) +CREATE +(TomC)-[:ACTED_IN {roles:['Jerry Maguire']}]->(JerryMaguire), +(CubaG)-[:ACTED_IN {roles:['Rod Tidwell']}]->(JerryMaguire), +(ReneeZ)-[:ACTED_IN {roles:['Dorothy Boyd']}]->(JerryMaguire), +(KellyP)-[:ACTED_IN {roles:['Avery Bishop']}]->(JerryMaguire), +(JerryO)-[:ACTED_IN {roles:['Frank Cushman']}]->(JerryMaguire), +(JayM)-[:ACTED_IN {roles:['Bob Sugar']}]->(JerryMaguire), +(BonnieH)-[:ACTED_IN {roles:['Laurel Boyd']}]->(JerryMaguire), +(ReginaK)-[:ACTED_IN {roles:['Marcee Tidwell']}]->(JerryMaguire), +(JonathanL)-[:ACTED_IN {roles:['Ray Boyd']}]->(JerryMaguire), +(CameronC)-[:DIRECTED]->(JerryMaguire), +(CameronC)-[:PRODUCED]->(JerryMaguire), +(CameronC)-[:WROTE]->(JerryMaguire) + +CREATE (StandByMe:Movie {title:"Stand By Me", released:1986, tagline:"For some, it's the last real taste of innocence, and the first real taste of life. But for everyone, it's the time that memories are made of."}) +CREATE (RiverP:Person {name:'River Phoenix', born:1970}) +CREATE (CoreyF:Person {name:'Corey Feldman', born:1971}) +CREATE (WilW:Person {name:'Wil Wheaton', born:1972}) +CREATE (JohnC:Person {name:'John Cusack', born:1966}) +CREATE (MarshallB:Person {name:'Marshall Bell', born:1942}) +CREATE +(WilW)-[:ACTED_IN {roles:['Gordie Lachance']}]->(StandByMe), +(RiverP)-[:ACTED_IN {roles:['Chris Chambers']}]->(StandByMe), +(JerryO)-[:ACTED_IN {roles:['Vern Tessio']}]->(StandByMe), +(CoreyF)-[:ACTED_IN {roles:['Teddy Duchamp']}]->(StandByMe), +(JohnC)-[:ACTED_IN {roles:['Denny Lachance']}]->(StandByMe), +(KieferS)-[:ACTED_IN {roles:['Ace Merrill']}]->(StandByMe), +(MarshallB)-[:ACTED_IN {roles:['Mr. Lachance']}]->(StandByMe), +(RobR)-[:DIRECTED]->(StandByMe) + +CREATE (AsGoodAsItGets:Movie {title:'As Good as It Gets', released:1997, tagline:'A comedy from the heart that goes for the throat.'}) +CREATE (HelenH:Person {name:'Helen Hunt', born:1963}) +CREATE (GregK:Person {name:'Greg Kinnear', born:1963}) +CREATE (JamesB:Person {name:'James L. Brooks', born:1940}) +CREATE +(JackN)-[:ACTED_IN {roles:['Melvin Udall']}]->(AsGoodAsItGets), +(HelenH)-[:ACTED_IN {roles:['Carol Connelly']}]->(AsGoodAsItGets), +(GregK)-[:ACTED_IN {roles:['Simon Bishop']}]->(AsGoodAsItGets), +(CubaG)-[:ACTED_IN {roles:['Frank Sachs']}]->(AsGoodAsItGets), +(JamesB)-[:DIRECTED]->(AsGoodAsItGets) + +CREATE (WhatDreamsMayCome:Movie {title:'What Dreams May Come', released:1998, tagline:'After life there is more. The end is just the beginning.'}) +CREATE (AnnabellaS:Person {name:'Annabella Sciorra', born:1960}) +CREATE (MaxS:Person {name:'Max von Sydow', born:1929}) +CREATE (WernerH:Person {name:'Werner Herzog', born:1942}) +CREATE (Robin:Person {name:'Robin Williams', born:1951}) +CREATE (VincentW:Person {name:'Vincent Ward', born:1956}) +CREATE +(Robin)-[:ACTED_IN {roles:['Chris Nielsen']}]->(WhatDreamsMayCome), +(CubaG)-[:ACTED_IN {roles:['Albert Lewis']}]->(WhatDreamsMayCome), +(AnnabellaS)-[:ACTED_IN {roles:['Annie Collins-Nielsen']}]->(WhatDreamsMayCome), +(MaxS)-[:ACTED_IN {roles:['The Tracker']}]->(WhatDreamsMayCome), +(WernerH)-[:ACTED_IN {roles:['The Face']}]->(WhatDreamsMayCome), +(VincentW)-[:DIRECTED]->(WhatDreamsMayCome) + +CREATE (SnowFallingonCedars:Movie {title:'Snow Falling on Cedars', released:1999, tagline:'First loves last. Forever.'}) +CREATE (EthanH:Person {name:'Ethan Hawke', born:1970}) +CREATE (RickY:Person {name:'Rick Yune', born:1971}) +CREATE (JamesC:Person {name:'James Cromwell', born:1940}) +CREATE (ScottH:Person {name:'Scott Hicks', born:1953}) +CREATE +(EthanH)-[:ACTED_IN {roles:['Ishmael Chambers']}]->(SnowFallingonCedars), +(RickY)-[:ACTED_IN {roles:['Kazuo Miyamoto']}]->(SnowFallingonCedars), +(MaxS)-[:ACTED_IN {roles:['Nels Gudmundsson']}]->(SnowFallingonCedars), +(JamesC)-[:ACTED_IN {roles:['Judge Fielding']}]->(SnowFallingonCedars), +(ScottH)-[:DIRECTED]->(SnowFallingonCedars) + +CREATE (YouveGotMail:Movie {title:"You've Got Mail", released:1998, tagline:'At odds in life... in love on-line.'}) +CREATE (ParkerP:Person {name:'Parker Posey', born:1968}) +CREATE (DaveC:Person {name:'Dave Chappelle', born:1973}) +CREATE (SteveZ:Person {name:'Steve Zahn', born:1967}) +CREATE (TomH:Person {name:'Tom Hanks', born:1956}) +CREATE (NoraE:Person {name:'Nora Ephron', born:1941}) +CREATE +(TomH)-[:ACTED_IN {roles:['Joe Fox']}]->(YouveGotMail), +(MegR)-[:ACTED_IN {roles:['Kathleen Kelly']}]->(YouveGotMail), +(GregK)-[:ACTED_IN {roles:['Frank Navasky']}]->(YouveGotMail), +(ParkerP)-[:ACTED_IN {roles:['Patricia Eden']}]->(YouveGotMail), +(DaveC)-[:ACTED_IN {roles:['Kevin Jackson']}]->(YouveGotMail), +(SteveZ)-[:ACTED_IN {roles:['George Pappas']}]->(YouveGotMail), +(NoraE)-[:DIRECTED]->(YouveGotMail) + +CREATE (SleeplessInSeattle:Movie {title:'Sleepless in Seattle', released:1993, tagline:'What if someone you never met, someone you never saw, someone you never knew was the only someone for you?'}) +CREATE (RitaW:Person {name:'Rita Wilson', born:1956}) +CREATE (BillPull:Person {name:'Bill Pullman', born:1953}) +CREATE (VictorG:Person {name:'Victor Garber', born:1949}) +CREATE (RosieO:Person {name:"Rosie O'Donnell", born:1962}) +CREATE +(TomH)-[:ACTED_IN {roles:['Sam Baldwin']}]->(SleeplessInSeattle), +(MegR)-[:ACTED_IN {roles:['Annie Reed']}]->(SleeplessInSeattle), +(RitaW)-[:ACTED_IN {roles:['Suzy']}]->(SleeplessInSeattle), +(BillPull)-[:ACTED_IN {roles:['Walter']}]->(SleeplessInSeattle), +(VictorG)-[:ACTED_IN {roles:['Greg']}]->(SleeplessInSeattle), +(RosieO)-[:ACTED_IN {roles:['Becky']}]->(SleeplessInSeattle), +(NoraE)-[:DIRECTED]->(SleeplessInSeattle) + +CREATE (JoeVersustheVolcano:Movie {title:'Joe Versus the Volcano', released:1990, tagline:'A story of love, lava and burning desire.'}) +CREATE (JohnS:Person {name:'John Patrick Stanley', born:1950}) +CREATE (Nathan:Person {name:'Nathan Lane', born:1956}) +CREATE +(TomH)-[:ACTED_IN {roles:['Joe Banks']}]->(JoeVersustheVolcano), +(MegR)-[:ACTED_IN {roles:['DeDe', 'Angelica Graynamore', 'Patricia Graynamore']}]->(JoeVersustheVolcano), +(Nathan)-[:ACTED_IN {roles:['Baw']}]->(JoeVersustheVolcano), +(JohnS)-[:DIRECTED]->(JoeVersustheVolcano) + +CREATE (WhenHarryMetSally:Movie {title:'When Harry Met Sally', released:1998, tagline:'Can two friends sleep together and still love each other in the morning?'}) +CREATE (BillyC:Person {name:'Billy Crystal', born:1948}) +CREATE (CarrieF:Person {name:'Carrie Fisher', born:1956}) +CREATE (BrunoK:Person {name:'Bruno Kirby', born:1949}) +CREATE +(BillyC)-[:ACTED_IN {roles:['Harry Burns']}]->(WhenHarryMetSally), +(MegR)-[:ACTED_IN {roles:['Sally Albright']}]->(WhenHarryMetSally), +(CarrieF)-[:ACTED_IN {roles:['Marie']}]->(WhenHarryMetSally), +(BrunoK)-[:ACTED_IN {roles:['Jess']}]->(WhenHarryMetSally), +(RobR)-[:DIRECTED]->(WhenHarryMetSally), +(RobR)-[:PRODUCED]->(WhenHarryMetSally), +(NoraE)-[:PRODUCED]->(WhenHarryMetSally), +(NoraE)-[:WROTE]->(WhenHarryMetSally) + +CREATE (ThatThingYouDo:Movie {title:'That Thing You Do', released:1996, tagline:'In every life there comes a time when that thing you dream becomes that thing you do'}) +CREATE (LivT:Person {name:'Liv Tyler', born:1977}) +CREATE +(TomH)-[:ACTED_IN {roles:['Mr. White']}]->(ThatThingYouDo), +(LivT)-[:ACTED_IN {roles:['Faye Dolan']}]->(ThatThingYouDo), +(Charlize)-[:ACTED_IN {roles:['Tina']}]->(ThatThingYouDo), +(TomH)-[:DIRECTED]->(ThatThingYouDo) + +CREATE (TheReplacements:Movie {title:'The Replacements', released:2000, tagline:'Pain heals, Chicks dig scars... Glory lasts forever'}) +CREATE (Brooke:Person {name:'Brooke Langton', born:1970}) +CREATE (Gene:Person {name:'Gene Hackman', born:1930}) +CREATE (Orlando:Person {name:'Orlando Jones', born:1968}) +CREATE (Howard:Person {name:'Howard Deutch', born:1950}) +CREATE +(Keanu)-[:ACTED_IN {roles:['Shane Falco']}]->(TheReplacements), +(Brooke)-[:ACTED_IN {roles:['Annabelle Farrell']}]->(TheReplacements), +(Gene)-[:ACTED_IN {roles:['Jimmy McGinty']}]->(TheReplacements), +(Orlando)-[:ACTED_IN {roles:['Clifford Franklin']}]->(TheReplacements), +(Howard)-[:DIRECTED]->(TheReplacements) + +CREATE (RescueDawn:Movie {title:'RescueDawn', released:2006, tagline:"Based on the extraordinary true story of one man's fight for freedom"}) +CREATE (ChristianB:Person {name:'Christian Bale', born:1974}) +CREATE (ZachG:Person {name:'Zach Grenier', born:1954}) +CREATE +(MarshallB)-[:ACTED_IN {roles:['Admiral']}]->(RescueDawn), +(ChristianB)-[:ACTED_IN {roles:['Dieter Dengler']}]->(RescueDawn), +(ZachG)-[:ACTED_IN {roles:['Squad Leader']}]->(RescueDawn), +(SteveZ)-[:ACTED_IN {roles:['Duane']}]->(RescueDawn), +(WernerH)-[:DIRECTED]->(RescueDawn) + +CREATE (TheBirdcage:Movie {title:'The Birdcage', released:1996, tagline:'Come as you are'}) +CREATE (MikeN:Person {name:'Mike Nichols', born:1931}) +CREATE +(Robin)-[:ACTED_IN {roles:['Armand Goldman']}]->(TheBirdcage), +(Nathan)-[:ACTED_IN {roles:['Albert Goldman']}]->(TheBirdcage), +(Gene)-[:ACTED_IN {roles:['Sen. Kevin Keeley']}]->(TheBirdcage), +(MikeN)-[:DIRECTED]->(TheBirdcage) + +CREATE (Unforgiven:Movie {title:'Unforgiven', released:1992, tagline:"It's a hell of a thing, killing a man"}) +CREATE (RichardH:Person {name:'Richard Harris', born:1930}) +CREATE (ClintE:Person {name:'Clint Eastwood', born:1930}) +CREATE +(RichardH)-[:ACTED_IN {roles:['English Bob']}]->(Unforgiven), +(ClintE)-[:ACTED_IN {roles:['Bill Munny']}]->(Unforgiven), +(Gene)-[:ACTED_IN {roles:['Little Bill Daggett']}]->(Unforgiven), +(ClintE)-[:DIRECTED]->(Unforgiven) + +CREATE (JohnnyMnemonic:Movie {title:'Johnny Mnemonic', released:1995, tagline:'The hottest data on earth. In the coolest head in town'}) +CREATE (Takeshi:Person {name:'Takeshi Kitano', born:1947}) +CREATE (Dina:Person {name:'Dina Meyer', born:1968}) +CREATE (IceT:Person {name:'Ice-T', born:1958}) +CREATE (RobertL:Person {name:'Robert Longo', born:1953}) +CREATE +(Keanu)-[:ACTED_IN {roles:['Johnny Mnemonic']}]->(JohnnyMnemonic), +(Takeshi)-[:ACTED_IN {roles:['Takahashi']}]->(JohnnyMnemonic), +(Dina)-[:ACTED_IN {roles:['Jane']}]->(JohnnyMnemonic), +(IceT)-[:ACTED_IN {roles:['J-Bone']}]->(JohnnyMnemonic), +(RobertL)-[:DIRECTED]->(JohnnyMnemonic) + +CREATE (CloudAtlas:Movie {title:'Cloud Atlas', released:2012, tagline:'Everything is connected'}) +CREATE (HalleB:Person {name:'Halle Berry', born:1966}) +CREATE (JimB:Person {name:'Jim Broadbent', born:1949}) +CREATE (TomT:Person {name:'Tom Tykwer', born:1965}) +CREATE (DavidMitchell:Person {name:'David Mitchell', born:1969}) +CREATE (StefanArndt:Person {name:'Stefan Arndt', born:1961}) +CREATE +(TomH)-[:ACTED_IN {roles:['Zachry', 'Dr. Henry Goose', 'Isaac Sachs', 'Dermot Hoggins']}]->(CloudAtlas), +(Hugo)-[:ACTED_IN {roles:['Bill Smoke', 'Haskell Moore', 'Tadeusz Kesselring', 'Nurse Noakes', 'Boardman Mephi', 'Old Georgie']}]->(CloudAtlas), +(HalleB)-[:ACTED_IN {roles:['Luisa Rey', 'Jocasta Ayrs', 'Ovid', 'Meronym']}]->(CloudAtlas), +(JimB)-[:ACTED_IN {roles:['Vyvyan Ayrs', 'Captain Molyneux', 'Timothy Cavendish']}]->(CloudAtlas), +(TomT)-[:DIRECTED]->(CloudAtlas), +(LillyW)-[:DIRECTED]->(CloudAtlas), +(LanaW)-[:DIRECTED]->(CloudAtlas), +(DavidMitchell)-[:WROTE]->(CloudAtlas), +(StefanArndt)-[:PRODUCED]->(CloudAtlas) + +CREATE (TheDaVinciCode:Movie {title:'The Da Vinci Code', released:2006, tagline:'Break The Codes'}) +CREATE (IanM:Person {name:'Ian McKellen', born:1939}) +CREATE (AudreyT:Person {name:'Audrey Tautou', born:1976}) +CREATE (PaulB:Person {name:'Paul Bettany', born:1971}) +CREATE (RonH:Person {name:'Ron Howard', born:1954}) +CREATE +(TomH)-[:ACTED_IN {roles:['Dr. Robert Langdon']}]->(TheDaVinciCode), +(IanM)-[:ACTED_IN {roles:['Sir Leight Teabing']}]->(TheDaVinciCode), +(AudreyT)-[:ACTED_IN {roles:['Sophie Neveu']}]->(TheDaVinciCode), +(PaulB)-[:ACTED_IN {roles:['Silas']}]->(TheDaVinciCode), +(RonH)-[:DIRECTED]->(TheDaVinciCode) + +CREATE (VforVendetta:Movie {title:'V for Vendetta', released:2006, tagline:'Freedom! Forever!'}) +CREATE (NatalieP:Person {name:'Natalie Portman', born:1981}) +CREATE (StephenR:Person {name:'Stephen Rea', born:1946}) +CREATE (JohnH:Person {name:'John Hurt', born:1940}) +CREATE (BenM:Person {name: 'Ben Miles', born:1967}) +CREATE +(Hugo)-[:ACTED_IN {roles:['V']}]->(VforVendetta), +(NatalieP)-[:ACTED_IN {roles:['Evey Hammond']}]->(VforVendetta), +(StephenR)-[:ACTED_IN {roles:['Eric Finch']}]->(VforVendetta), +(JohnH)-[:ACTED_IN {roles:['High Chancellor Adam Sutler']}]->(VforVendetta), +(BenM)-[:ACTED_IN {roles:['Dascomb']}]->(VforVendetta), +(JamesM)-[:DIRECTED]->(VforVendetta), +(LillyW)-[:PRODUCED]->(VforVendetta), +(LanaW)-[:PRODUCED]->(VforVendetta), +(JoelS)-[:PRODUCED]->(VforVendetta), +(LillyW)-[:WROTE]->(VforVendetta), +(LanaW)-[:WROTE]->(VforVendetta) + +CREATE (SpeedRacer:Movie {title:'Speed Racer', released:2008, tagline:'Speed has no limits'}) +CREATE (EmileH:Person {name:'Emile Hirsch', born:1985}) +CREATE (JohnG:Person {name:'John Goodman', born:1960}) +CREATE (SusanS:Person {name:'Susan Sarandon', born:1946}) +CREATE (MatthewF:Person {name:'Matthew Fox', born:1966}) +CREATE (ChristinaR:Person {name:'Christina Ricci', born:1980}) +CREATE (Rain:Person {name:'Rain', born:1982}) +CREATE +(EmileH)-[:ACTED_IN {roles:['Speed Racer']}]->(SpeedRacer), +(JohnG)-[:ACTED_IN {roles:['Pops']}]->(SpeedRacer), +(SusanS)-[:ACTED_IN {roles:['Mom']}]->(SpeedRacer), +(MatthewF)-[:ACTED_IN {roles:['Racer X']}]->(SpeedRacer), +(ChristinaR)-[:ACTED_IN {roles:['Trixie']}]->(SpeedRacer), +(Rain)-[:ACTED_IN {roles:['Taejo Togokahn']}]->(SpeedRacer), +(BenM)-[:ACTED_IN {roles:['Cass Jones']}]->(SpeedRacer), +(LillyW)-[:DIRECTED]->(SpeedRacer), +(LanaW)-[:DIRECTED]->(SpeedRacer), +(LillyW)-[:WROTE]->(SpeedRacer), +(LanaW)-[:WROTE]->(SpeedRacer), +(JoelS)-[:PRODUCED]->(SpeedRacer) + +CREATE (NinjaAssassin:Movie {title:'Ninja Assassin', released:2009, tagline:'Prepare to enter a secret world of assassins'}) +CREATE (NaomieH:Person {name:'Naomie Harris'}) +CREATE +(Rain)-[:ACTED_IN {roles:['Raizo']}]->(NinjaAssassin), +(NaomieH)-[:ACTED_IN {roles:['Mika Coretti']}]->(NinjaAssassin), +(RickY)-[:ACTED_IN {roles:['Takeshi']}]->(NinjaAssassin), +(BenM)-[:ACTED_IN {roles:['Ryan Maslow']}]->(NinjaAssassin), +(JamesM)-[:DIRECTED]->(NinjaAssassin), +(LillyW)-[:PRODUCED]->(NinjaAssassin), +(LanaW)-[:PRODUCED]->(NinjaAssassin), +(JoelS)-[:PRODUCED]->(NinjaAssassin) + +CREATE (TheGreenMile:Movie {title:'The Green Mile', released:1999, tagline:"Walk a mile you'll never forget."}) +CREATE (MichaelD:Person {name:'Michael Clarke Duncan', born:1957}) +CREATE (DavidM:Person {name:'David Morse', born:1953}) +CREATE (SamR:Person {name:'Sam Rockwell', born:1968}) +CREATE (GaryS:Person {name:'Gary Sinise', born:1955}) +CREATE (PatriciaC:Person {name:'Patricia Clarkson', born:1959}) +CREATE (FrankD:Person {name:'Frank Darabont', born:1959}) +CREATE +(TomH)-[:ACTED_IN {roles:['Paul Edgecomb']}]->(TheGreenMile), +(MichaelD)-[:ACTED_IN {roles:['John Coffey']}]->(TheGreenMile), +(DavidM)-[:ACTED_IN {roles:['Brutus "Brutal" Howell']}]->(TheGreenMile), +(BonnieH)-[:ACTED_IN {roles:['Jan Edgecomb']}]->(TheGreenMile), +(JamesC)-[:ACTED_IN {roles:['Warden Hal Moores']}]->(TheGreenMile), +(SamR)-[:ACTED_IN {roles:['"Wild Bill" Wharton']}]->(TheGreenMile), +(GaryS)-[:ACTED_IN {roles:['Burt Hammersmith']}]->(TheGreenMile), +(PatriciaC)-[:ACTED_IN {roles:['Melinda Moores']}]->(TheGreenMile), +(FrankD)-[:DIRECTED]->(TheGreenMile) + +CREATE (FrostNixon:Movie {title:'Frost/Nixon', released:2008, tagline:'400 million people were waiting for the truth.'}) +CREATE (FrankL:Person {name:'Frank Langella', born:1938}) +CREATE (MichaelS:Person {name:'Michael Sheen', born:1969}) +CREATE (OliverP:Person {name:'Oliver Platt', born:1960}) +CREATE +(FrankL)-[:ACTED_IN {roles:['Richard Nixon']}]->(FrostNixon), +(MichaelS)-[:ACTED_IN {roles:['David Frost']}]->(FrostNixon), +(KevinB)-[:ACTED_IN {roles:['Jack Brennan']}]->(FrostNixon), +(OliverP)-[:ACTED_IN {roles:['Bob Zelnick']}]->(FrostNixon), +(SamR)-[:ACTED_IN {roles:['James Reston, Jr.']}]->(FrostNixon), +(RonH)-[:DIRECTED]->(FrostNixon) + +CREATE (Hoffa:Movie {title:'Hoffa', released:1992, tagline:"He didn't want law. He wanted justice."}) +CREATE (DannyD:Person {name:'Danny DeVito', born:1944}) +CREATE (JohnR:Person {name:'John C. Reilly', born:1965}) +CREATE +(JackN)-[:ACTED_IN {roles:['Hoffa']}]->(Hoffa), +(DannyD)-[:ACTED_IN {roles:['Robert "Bobby" Ciaro']}]->(Hoffa), +(JTW)-[:ACTED_IN {roles:['Frank Fitzsimmons']}]->(Hoffa), +(JohnR)-[:ACTED_IN {roles:['Peter "Pete" Connelly']}]->(Hoffa), +(DannyD)-[:DIRECTED]->(Hoffa) + +CREATE (Apollo13:Movie {title:'Apollo 13', released:1995, tagline:'Houston, we have a problem.'}) +CREATE (EdH:Person {name:'Ed Harris', born:1950}) +CREATE (BillPax:Person {name:'Bill Paxton', born:1955}) +CREATE +(TomH)-[:ACTED_IN {roles:['Jim Lovell']}]->(Apollo13), +(KevinB)-[:ACTED_IN {roles:['Jack Swigert']}]->(Apollo13), +(EdH)-[:ACTED_IN {roles:['Gene Kranz']}]->(Apollo13), +(BillPax)-[:ACTED_IN {roles:['Fred Haise']}]->(Apollo13), +(GaryS)-[:ACTED_IN {roles:['Ken Mattingly']}]->(Apollo13), +(RonH)-[:DIRECTED]->(Apollo13) + +CREATE (Twister:Movie {title:'Twister', released:1996, tagline:"Don't Breathe. Don't Look Back."}) +CREATE (PhilipH:Person {name:'Philip Seymour Hoffman', born:1967}) +CREATE (JanB:Person {name:'Jan de Bont', born:1943}) +CREATE +(BillPax)-[:ACTED_IN {roles:['Bill Harding']}]->(Twister), +(HelenH)-[:ACTED_IN {roles:['Dr. Jo Harding']}]->(Twister), +(ZachG)-[:ACTED_IN {roles:['Eddie']}]->(Twister), +(PhilipH)-[:ACTED_IN {roles:['Dustin "Dusty" Davis']}]->(Twister), +(JanB)-[:DIRECTED]->(Twister) + +CREATE (CastAway:Movie {title:'Cast Away', released:2000, tagline:'At the edge of the world, his journey begins.'}) +CREATE (RobertZ:Person {name:'Robert Zemeckis', born:1951}) +CREATE +(TomH)-[:ACTED_IN {roles:['Chuck Noland']}]->(CastAway), +(HelenH)-[:ACTED_IN {roles:['Kelly Frears']}]->(CastAway), +(RobertZ)-[:DIRECTED]->(CastAway) + +CREATE (OneFlewOvertheCuckoosNest:Movie {title:"One Flew Over the Cuckoo's Nest", released:1975, tagline:"If he's crazy, what does that make you?"}) +CREATE (MilosF:Person {name:'Milos Forman', born:1932}) +CREATE +(JackN)-[:ACTED_IN {roles:['Randle McMurphy']}]->(OneFlewOvertheCuckoosNest), +(DannyD)-[:ACTED_IN {roles:['Martini']}]->(OneFlewOvertheCuckoosNest), +(MilosF)-[:DIRECTED]->(OneFlewOvertheCuckoosNest) + +CREATE (SomethingsGottaGive:Movie {title:"Something's Gotta Give", released:2003}) +CREATE (DianeK:Person {name:'Diane Keaton', born:1946}) +CREATE (NancyM:Person {name:'Nancy Meyers', born:1949}) +CREATE +(JackN)-[:ACTED_IN {roles:['Harry Sanborn']}]->(SomethingsGottaGive), +(DianeK)-[:ACTED_IN {roles:['Erica Barry']}]->(SomethingsGottaGive), +(Keanu)-[:ACTED_IN {roles:['Julian Mercer']}]->(SomethingsGottaGive), +(NancyM)-[:DIRECTED]->(SomethingsGottaGive), +(NancyM)-[:PRODUCED]->(SomethingsGottaGive), +(NancyM)-[:WROTE]->(SomethingsGottaGive) + +CREATE (BicentennialMan:Movie {title:'Bicentennial Man', released:1999, tagline:"One robot's 200 year journey to become an ordinary man."}) +CREATE (ChrisC:Person {name:'Chris Columbus', born:1958}) +CREATE +(Robin)-[:ACTED_IN {roles:['Andrew Marin']}]->(BicentennialMan), +(OliverP)-[:ACTED_IN {roles:['Rupert Burns']}]->(BicentennialMan), +(ChrisC)-[:DIRECTED]->(BicentennialMan) + +CREATE (CharlieWilsonsWar:Movie {title:"Charlie Wilson's War", released:2007, tagline:"A stiff drink. A little mascara. A lot of nerve. Who said they couldn't bring down the Soviet empire."}) +CREATE (JuliaR:Person {name:'Julia Roberts', born:1967}) +CREATE +(TomH)-[:ACTED_IN {roles:['Rep. Charlie Wilson']}]->(CharlieWilsonsWar), +(JuliaR)-[:ACTED_IN {roles:['Joanne Herring']}]->(CharlieWilsonsWar), +(PhilipH)-[:ACTED_IN {roles:['Gust Avrakotos']}]->(CharlieWilsonsWar), +(MikeN)-[:DIRECTED]->(CharlieWilsonsWar) + +CREATE (ThePolarExpress:Movie {title:'The Polar Express', released:2004, tagline:'This Holiday Season... Believe'}) +CREATE +(TomH)-[:ACTED_IN {roles:['Hero Boy', 'Father', 'Conductor', 'Hobo', 'Scrooge', 'Santa Claus']}]->(ThePolarExpress), +(RobertZ)-[:DIRECTED]->(ThePolarExpress) + +CREATE (ALeagueofTheirOwn:Movie {title:'A League of Their Own', released:1992, tagline:'Once in a lifetime you get a chance to do something different.'}) +CREATE (Madonna:Person {name:'Madonna', born:1954}) +CREATE (GeenaD:Person {name:'Geena Davis', born:1956}) +CREATE (LoriP:Person {name:'Lori Petty', born:1963}) +CREATE (PennyM:Person {name:'Penny Marshall', born:1943}) +CREATE +(TomH)-[:ACTED_IN {roles:['Jimmy Dugan']}]->(ALeagueofTheirOwn), +(GeenaD)-[:ACTED_IN {roles:['Dottie Hinson']}]->(ALeagueofTheirOwn), +(LoriP)-[:ACTED_IN {roles:['Kit Keller']}]->(ALeagueofTheirOwn), +(RosieO)-[:ACTED_IN {roles:['Doris Murphy']}]->(ALeagueofTheirOwn), +(Madonna)-[:ACTED_IN {roles:['"All the Way" Mae Mordabito']}]->(ALeagueofTheirOwn), +(BillPax)-[:ACTED_IN {roles:['Bob Hinson']}]->(ALeagueofTheirOwn), +(PennyM)-[:DIRECTED]->(ALeagueofTheirOwn) + +CREATE (PaulBlythe:Person {name:'Paul Blythe'}) +CREATE (AngelaScope:Person {name:'Angela Scope'}) +CREATE (JessicaThompson:Person {name:'Jessica Thompson'}) +CREATE (JamesThompson:Person {name:'James Thompson'}) + +CREATE +(JamesThompson)-[:FOLLOWS]->(JessicaThompson), +(AngelaScope)-[:FOLLOWS]->(JessicaThompson), +(PaulBlythe)-[:FOLLOWS]->(AngelaScope) + +CREATE +(JessicaThompson)-[:REVIEWED {summary:'An amazing journey', rating:95}]->(CloudAtlas), +(JessicaThompson)-[:REVIEWED {summary:'Silly, but fun', rating:65}]->(TheReplacements), +(JamesThompson)-[:REVIEWED {summary:'The coolest football movie ever', rating:100}]->(TheReplacements), +(AngelaScope)-[:REVIEWED {summary:'Pretty funny at times', rating:62}]->(TheReplacements), +(JessicaThompson)-[:REVIEWED {summary:'Dark, but compelling', rating:85}]->(Unforgiven), +(JessicaThompson)-[:REVIEWED {summary:"Slapstick redeemed only by the Robin Williams and Gene Hackman's stellar performances", rating:45}]->(TheBirdcage), +(JessicaThompson)-[:REVIEWED {summary:'A solid romp', rating:68}]->(TheDaVinciCode), +(JamesThompson)-[:REVIEWED {summary:'Fun, but a little far fetched', rating:65}]->(TheDaVinciCode), +(JessicaThompson)-[:REVIEWED {summary:'You had me at Jerry', rating:92}]->(JerryMaguire); \ No newline at end of file diff --git a/docker/sso-keycloak/resources/neo4j-config/role-analyst.json b/docker/sso-keycloak/resources/neo4j-config/role-analyst.json new file mode 100644 index 00000000000..345c9372f5a --- /dev/null +++ b/docker/sso-keycloak/resources/neo4j-config/role-analyst.json @@ -0,0 +1,41 @@ +{ + "kind": "Role", + "name": "analyst", + "dropIfExists": true, + "privileges": [ + { + "graph": "movies", + "access": true, + "rules": [ + { + "target": "node", + "labels": "*", + "action": "match", + "resource": "all_properties", + "access": "GRANTED" + }, + { + "target": "relationship", + "labels": "*", + "action": "read", + "resource": "*", + "access": "GRANTED" + }, + { + "target": "relationship", + "labels": "*", + "action": "traverse", + "resource": "*", + "access": "GRANTED" + }, + { + "target": "node", + "labels": "Person", + "action": "read", + "resource": "born", + "access": "DENIED" + } + ] + } + ] +} diff --git a/e2e_tests/sso/single-sign-on.spec.ts b/e2e_tests/sso/single-sign-on.spec.ts new file mode 100644 index 00000000000..9c0e9985322 --- /dev/null +++ b/e2e_tests/sso/single-sign-on.spec.ts @@ -0,0 +1,79 @@ +/* + * Copyright (c) Neo4j + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Neo4j is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +describe('Sign in with keycloak', () => { + before(function () { + cy.visit(Cypress.config('url')).title().should('include', 'Neo4j Browser') + cy.wait(3000) + }) + + it('can login', () => { + cy.executeCommand(':server disconnect') + cy.executeCommand(':clear') + cy.executeCommand(':server connect') + cy.get('[data-testid=boltaddress]').type('localhost:7687') + cy.get('[data-testid="authenticationMethod"]').select('Single Sign On') + cy.contains('Keycloak').click() + + // might do automatic redirect depending + cy.wait(5000) + cy.get('body').then($body => { + if ($body.find('#username').length > 0) { + cy.get('#username').type('admin') + cy.get('#password').type('password{enter}') + } + }) + + cy.title().should('include', 'Neo4j Browser') + cy.wait(3000) + cy.contains('You are connected').should('exist') + cy.executeCommand( + 'create (t :Location {{}name: "Ambrette Town"{}}) return t.name' + ) + cy.waitForCommandResult() + cy.resultContains('Ambrette Town') + cy.executeCommand(':debug') + cy.contains('SSO Connection to Neo4j successfully established').should( + 'exist' + ) + + cy.wait(7 * 60 * 1000) + + cy.executeCommand(':clear') + + // first command after timeout may fail + cy.executeCommand('RETURN 123') + cy.waitForCommandResult() + + cy.executeCommand( + 'create (t :Location {{}name: "Pastoria City"{}}) return t.name' + ) + cy.waitForCommandResult() + cy.resultContains('Pastoria City') + cy.executeCommand(':clear') + // sidebar has new label + cy.get('[data-testid="navigationDBMS"]').click() + cy.contains('Location') + + cy.executeCommand(':debug') + cy.contains('Successfully refreshed token').should('exist') + cy.contains('Connection recovered successfully').should('exist') + }) +}) diff --git a/package.json b/package.json index eb28c79d69b..3a20cb2390b 100644 --- a/package.json +++ b/package.json @@ -29,6 +29,8 @@ "e2e-aura-open": "CYPRESS_E2E_TEST_ENV=\"aura\" CYPRESS_BASE_URL=\"https://localhost:8080\" cypress open", "e2e-local": "CYPRESS_E2E_TEST_ENV=\"local\" cypress run", "e2e-local-open": "CYPRESS_E2E_TEST_ENV=\"local\" cypress open", + "e2e-sso": "cypress run --config integrationFolder=\"./e2e_tests/sso/\"", + "e2e-sso-open": "cypress open --config integrationFolder=\"./e2e_tests/sso/\"", "format": "prettier-eslint --write 'src/**/!(*.min).{js,jsx,ts,tsx,css,json}' 'e2e_tests/**/*.{js,jsx,ts,tsx,css,json}' 'scripts/**/*.{js,jsx,ts,tsx,css,json}' 'build_scripts/**/*.{js,jsx,ts,tsx,css,json}'", "jest": "jest", "jest-cov": "jest --coverage", diff --git a/src/browser/modules/Stream/CypherFrame/RelatableView/__snapshots__/relatable-view.test.tsx.snap b/src/browser/modules/Stream/CypherFrame/RelatableView/__snapshots__/relatable-view.test.tsx.snap index 15425d58442..33007397161 100644 --- a/src/browser/modules/Stream/CypherFrame/RelatableView/__snapshots__/relatable-view.test.tsx.snap +++ b/src/browser/modules/Stream/CypherFrame/RelatableView/__snapshots__/relatable-view.test.tsx.snap @@ -6,7 +6,7 @@ exports[`RelatableViews RelatableView displays bodyMessage if no rows 1`] = ` class="sc-jeraig dxzVkG" >
(no changes, no records)
@@ -17,10 +17,10 @@ exports[`RelatableViews RelatableView displays bodyMessage if no rows 1`] = ` exports[`RelatableViews RelatableView does not crash if key is empty string 1`] = `
"String value" @@ -89,10 +89,10 @@ exports[`RelatableViews RelatableView does not crash if key is empty string 1`] exports[`RelatableViews RelatableView does not display bodyMessage if rows, and escapes HTML 1`] = `
"String with HTML <strong>in</strong> it" @@ -172,7 +172,7 @@ exports[`RelatableViews TableStatusbar displays statusBarMessage 1`] = ` class="sc-jeraig dxzVkG" >
Started streaming 1 records after 5 ms and completed after 10 ms. diff --git a/src/browser/modules/Stream/CypherFrame/__snapshots__/AsciiView.test.tsx.snap b/src/browser/modules/Stream/CypherFrame/__snapshots__/AsciiView.test.tsx.snap index 69cbaa7ad8e..ec5111e4766 100644 --- a/src/browser/modules/Stream/CypherFrame/__snapshots__/AsciiView.test.tsx.snap +++ b/src/browser/modules/Stream/CypherFrame/__snapshots__/AsciiView.test.tsx.snap @@ -6,7 +6,7 @@ exports[`AsciiViews AsciiStatusbar displays statusBarMessage if no rows 1`] = ` class="sc-jeraig dxzVkG" >
Completed after 10 ms.
diff --git a/src/browser/modules/Stream/CypherFrame/__snapshots__/CodeView.test.tsx.snap b/src/browser/modules/Stream/CypherFrame/__snapshots__/CodeView.test.tsx.snap index 02aa51077eb..b0e94bbd504 100644 --- a/src/browser/modules/Stream/CypherFrame/__snapshots__/CodeView.test.tsx.snap +++ b/src/browser/modules/Stream/CypherFrame/__snapshots__/CodeView.test.tsx.snap @@ -14,7 +14,7 @@ exports[`CodeViews CodeStatusbar displays statusBarMessage 1`] = ` class="sc-jcFjpl cdUBmZ" >
Started streaming 1 records after 5 ms and completed after 10 ms. diff --git a/src/shared/modules/connections/connectionsDuck.ts b/src/shared/modules/connections/connectionsDuck.ts index 866f7aa9841..be1e8bcc228 100644 --- a/src/shared/modules/connections/connectionsDuck.ts +++ b/src/shared/modules/connections/connectionsDuck.ts @@ -101,6 +101,7 @@ export type Connection = { restApi?: string SSOError?: string SSOProviders?: SSOProvider[] + attemptSSOLogin?: boolean } export const initialState: ConnectionReduxState = { @@ -352,14 +353,6 @@ export const onLostConnection = (dispatch: any) => (e: any) => { dispatch({ type: LOST_CONNECTION, error: e }) } -export const connectionLossFilter = (action: any) => { - const notLostCodes = [ - 'Neo.ClientError.Security.Unauthorized', - 'Neo.ClientError.Security.AuthenticationRateLimit' - ] - return notLostCodes.indexOf(action.error.code) < 0 -} - export const setRetainCredentials = (shouldRetain: any) => { return { type: UPDATE_RETAIN_CREDENTIALS, @@ -408,7 +401,8 @@ export const connectEpic = (action$: any, store: any) => try { await bolt.backgroundWorkerlessRoutedRead( supportsMultiDb ? 'SHOW DATABASES' : 'call db.indexes()', - { useDb: supportsMultiDb ? 'SYSTEM' : undefined } + { useDb: supportsMultiDb ? 'SYSTEM' : undefined }, + store ) } catch (error) { const e: any = error @@ -529,6 +523,9 @@ export const startupConnectEpic = (action$: any, store: any) => { onLostConnection(store.dispatch) ) store.dispatch(setActiveConnection(discovery.CONNECTION_ID)) + authLog( + 'Neo4j Browser successfully connected to Neo4j Server with stored credentials' + ) return { type: STARTUP_CONNECTION_SUCCESS } } catch {} } @@ -548,6 +545,12 @@ export const startupConnectEpic = (action$: any, store: any) => { if (shouldTryAutoconnecting(connUpdatedWithDiscovery)) { // Try connecting + + if (discovered.attemptSSOLogin) { + authLog( + 'Attempting to establish SSO connection to Neo4j with assembled credentials.' + ) + } return new Promise(resolve => { // Try to connect with stored creds bolt @@ -558,12 +561,15 @@ export const startupConnectEpic = (action$: any, store: any) => { ) .then(() => { store.dispatch(setActiveConnection(discovery.CONNECTION_ID)) + if (discovered.attemptSSOLogin) { + authLog('SSO Connection to Neo4j successfully established.') + } resolve({ type: STARTUP_CONNECTION_SUCCESS }) }) .catch(() => { if (discovered.attemptSSOLogin) { authLog( - 'client side SSO flow completed but Neo4j Browser failed to connect to neo4j. Server side logs (security.log or debug.log) may contain more information.' + 'SSO Connection to Neo4j failed, although the client side SSO flow succeeded. Server side logs (security.log or debug.log) may contain more information.' ) } store.dispatch(setActiveConnection(null)) @@ -667,12 +673,12 @@ export const disconnectSuccessEpic = (action$: any, store: any) => { export const connectionLostEpic = (action$: any, store: any) => action$ .ofType(LOST_CONNECTION) - .filter(connectionLossFilter) // Only retry in web env and if we're supposed to be connected .filter(() => inWebEnv(store.getState()) && isConnected(store.getState())) .throttleTime(5000) .do(() => store.dispatch(updateConnectionState(PENDING_STATE))) .mergeMap((action: any) => { + authLog('Detected loss of connectitivity, attempting to recover') return ( Rx.Observable.of(1) .mergeMap(() => { @@ -737,6 +743,7 @@ export const connectionLostEpic = (action$: any, store: any) => onLostConnection(store.dispatch) ) .then(() => { + authLog('Connection recovered successfully.') store.dispatch(updateConnectionState(CONNECTED_STATE)) resolve({ type: 'Success' }) }) @@ -745,7 +752,45 @@ export const connectionLostEpic = (action$: any, store: any) => .catch(e => { // Don't retry if auth failed if (e.code === UnauthorizedDriverError) { - resolve({ type: e.code }) + // except in rare cases where the auth token has expired + // but we didn't catch the auth token expired exception. + // if we signed in with SSO, try to refresh the token here as well + if (connection?.attemptSSOLogin) { + authLog( + 'Client was unauthorized, could be due to access token expiry, starting refresh attempt' + ) + const SSOProviders = getActiveConnectionData( + store.getState() + )?.SSOProviders + if (SSOProviders) { + handleRefreshingToken(SSOProviders) + .then(credentials => { + store.dispatch( + discovery.updateDiscoveryConnection(credentials) + ) + connection = getActiveConnectionData( + store.getState() + ) + authLog( + 'Successfully refreshed token, attempting to reconnect' + ) + return reject(new Error('Try again with new token')) + }) + .catch(e => { + // sso-lib throws errors with simple strings + authLog( + 'Token refresh attempt failed: ' + String(e) + ) + // if refreshing the token failed, don't retry connectivity recover + return resolve({ type: UnauthorizedDriverError }) + }) + } + } else { + authLog( + 'Client was unauthorized, stopping reconnection attempts' + ) + resolve({ type: e.code }) + } } else { setTimeout( () => reject(new Error('Couldnt reconnect.')), @@ -759,6 +804,7 @@ export const connectionLostEpic = (action$: any, store: any) => .catch(() => { bolt.closeConnection() store.dispatch(setActiveConnection(null)) + authLog('Failed to recover connectivity.') return Rx.Observable.of(null) }) // It can be resolved for a number of reasons: diff --git a/src/shared/modules/currentUser/currentUserDuck.ts b/src/shared/modules/currentUser/currentUserDuck.ts index 859156fb733..7ff79fecff3 100644 --- a/src/shared/modules/currentUser/currentUserDuck.ts +++ b/src/shared/modules/currentUser/currentUserDuck.ts @@ -103,7 +103,8 @@ export const getCurrentUserEpic = (some$: any, store: any) => getShowCurrentUserProcedure( hasMultidb ? FIRST_MULTI_DB_SUPPORT : FIRST_NO_MULTI_DB_SUPPORT ), - { useDb: hasMultidb ? SYSTEM_DB : undefined } + { useDb: hasMultidb ? SYSTEM_DB : undefined }, + store ) return resolve(res) diff --git a/src/shared/modules/dbMeta/dbMetaEpics.ts b/src/shared/modules/dbMeta/dbMetaEpics.ts index f17688d1369..3bfeaf7472f 100644 --- a/src/shared/modules/dbMeta/dbMetaEpics.ts +++ b/src/shared/modules/dbMeta/dbMetaEpics.ts @@ -74,7 +74,6 @@ import { LOST_CONNECTION, SILENT_DISCONNECT, UPDATE_CONNECTION_STATE, - connectionLossFilter, getActiveConnectionData, getLastUseDb, getUseDb, @@ -97,6 +96,13 @@ import { isSystemOrCompositeDb, getCurrentDatabase } from 'shared/utils/selectors' +import { isBoltConnectionErrorCode } from 'services/bolt/boltConnectionErrors' + +function handleConnectionError(store: any, e: any) { + if (!e.code || isBoltConnectionErrorCode(e.code)) { + onLostConnection(store.dispatch)(e) + } +} async function databaseList(store: any) { try { @@ -105,9 +111,13 @@ async function databaseList(store: any) { return } - const res = await bolt.backgroundWorkerlessRoutedRead('SHOW DATABASES', { - useDb: SYSTEM_DB - }) + const res = await bolt.backgroundWorkerlessRoutedRead( + 'SHOW DATABASES', + { + useDb: SYSTEM_DB + }, + store + ) if (!res) return @@ -135,9 +145,13 @@ async function getLabelsAndTypes(store: any) { // Not system db, try and fetch meta data try { - const res = await bolt.backgroundWorkerlessRoutedRead(metaTypesQuery, { - useDb: db?.name - }) + const res = await bolt.backgroundWorkerlessRoutedRead( + metaTypesQuery, + { + useDb: db?.name + }, + store + ) if (res && res.records && res.records.length !== 0) { const [rawLabels, rawRelTypes, rawProperties] = res.records.map( (r: Record) => r.get(0).data @@ -173,9 +187,13 @@ async function getNodeAndRelationshipCounts( // Not system db, try and fetch meta data try { - const res = await bolt.backgroundWorkerlessRoutedRead(metaCountQuery, { - useDb: db?.name - }) + const res = await bolt.backgroundWorkerlessRoutedRead( + metaCountQuery, + { + useDb: db?.name + }, + store + ) if (res && res.records && res.records.length !== 0) { const [rawNodeCount, rawRelationshipCount] = res.records.map( (r: Record) => r.get(0).data @@ -210,11 +228,13 @@ async function getFunctionsAndProcedures(store: any) { const useDb = supportsMultiDb(store.getState()) ? SYSTEM_DB : undefined const procedurePromise = bolt.backgroundWorkerlessRoutedRead( getListProcedureQuery(version), - { useDb } + { useDb }, + store ) const functionPromise = bolt.backgroundWorkerlessRoutedRead( getListFunctionQuery(version), - { useDb } + { useDb }, + store ) const [procedures, functions] = await Promise.all([ procedurePromise, @@ -227,7 +247,7 @@ async function getFunctionsAndProcedures(store: any) { functions: functions.records.map(f => f.toObject()) }) ) - } catch (e) {} + } catch {} } async function clusterRole(store: any) { @@ -251,7 +271,9 @@ async function clusterRole(store: any) { const role = res.records[0].get(0) store.dispatch(update({ role })) - } catch {} + } catch (e) { + handleConnectionError(store, e) + } } async function fetchServerInfo(store: any) { @@ -259,7 +281,8 @@ async function fetchServerInfo(store: any) { const serverInfo = await bolt.backgroundWorkerlessRoutedRead( serverInfoQuery, // We use the bolt method for multi db support, since don't have the version in redux yet - { useDb: (await bolt.hasMultiDbSupport()) ? SYSTEM_DB : undefined } + { useDb: (await bolt.hasMultiDbSupport()) ? SYSTEM_DB : undefined }, + store ) store.dispatch(updateServerInfo(serverInfo)) } catch {} @@ -278,7 +301,8 @@ async function fetchTrialStatus(store: any) { const trialStatus = await bolt.backgroundWorkerlessRoutedRead( trialStatusQuery, // System database is available from v4 - { useDb: SYSTEM_DB } + { useDb: SYSTEM_DB }, + store ) store.dispatch(updateTrialStatus(trialStatus)) } catch {} @@ -286,7 +310,8 @@ async function fetchTrialStatus(store: any) { try { const oldTrialStatus = await bolt.backgroundWorkerlessRoutedRead( oldTrialStatusQuery, - { useDb: SYSTEM_DB } + { useDb: SYSTEM_DB }, + store ) store.dispatch(updateTrialStatusOld(oldTrialStatus)) } catch {} @@ -397,7 +422,6 @@ export const dbMetaEpic = (some$: any, store: any) => .takeUntil( some$ .ofType(LOST_CONNECTION) - .filter(connectionLossFilter) .merge(some$.ofType(DISCONNECTION_SUCCESS)) .merge(some$.ofType(SILENT_DISCONNECT)) ) @@ -453,7 +477,8 @@ export const serverConfigEpic = (some$: any, store: any) => ? 'dbms.clientConfig()' : 'dbms.listConfig()' }`, - { useDb } + { useDb }, + store ) .then((r: any) => { // This is not set yet @@ -469,9 +494,13 @@ export const serverConfigEpic = (some$: any, store: any) => store.dispatch(setClientConfig(false)) bolt - .backgroundWorkerlessRoutedRead(`CALL dbms.listConfig()`, { - useDb - }) + .backgroundWorkerlessRoutedRead( + `CALL dbms.listConfig()`, + { + useDb + }, + store + ) .then(resolve) .catch(reject) } else { diff --git a/src/shared/services/bolt/bolt.ts b/src/shared/services/bolt/bolt.ts index 8a0bcfe3bac..71f692b1bd2 100644 --- a/src/shared/services/bolt/bolt.ts +++ b/src/shared/services/bolt/bolt.ts @@ -31,11 +31,15 @@ import { import { addTypesAsField, setupBoltWorker } from './setup-bolt-worker' import { cancelTransaction as globalCancelTransaction } from './transactions' import { NATIVE } from 'services/bolt/boltHelpers' -import { Connection } from 'shared/modules/connections/connectionsDuck' +import { + Connection, + onLostConnection +} from 'shared/modules/connections/connectionsDuck' import BoltWorkerModule from 'shared/services/bolt/boltWorker' import { backgroundTxMetadata } from './txMetadata' import { getGlobalDrivers } from './globalDrivers' import { BoltConnectionError } from 'services/exceptions' +import { isBoltConnectionErrorCode } from './boltConnectionErrors' let connectionProperties: {} | null = null let _useDb: string | null = null @@ -182,7 +186,8 @@ function directTransaction( async function backgroundWorkerlessRoutedRead( input: string, - { useDb }: { useDb?: string } + { useDb }: { useDb?: string }, + store: any ): Promise { const session = getGlobalDrivers() ?.getRoutedDriver() @@ -197,6 +202,12 @@ async function backgroundWorkerlessRoutedRead( .executeRead(tx => tx.run(input), { metadata: backgroundTxMetadata.txMetadata }) + .catch(e => { + if (!e.code || isBoltConnectionErrorCode(e.code)) { + onLostConnection(store.dispatch)(e) + } + throw e + }) .finally(() => session.close()) }