jpvanoosten / discord-user-manager

This is intended to be used as a Discord bot that manages invites to a Discord server based on a user's G suite account.
MIT License
15 stars 9 forks source link

Status? #1

Closed marfier closed 4 years ago

marfier commented 4 years ago

Hi,

Is this project still being maintained? Any possibility for a bit of documentation and context?

Thanks.

jpvanoosten commented 4 years ago

Thanks for your interest.

I'm currently working on the README.md in the dev branch. See 566a0ab.

There are a few more things on the road-map such as adding external users (currently the user manager admin interface is not yet finished), kicking users from the user manager interface, adding admins, Office 365 logins, and other features that are not quite ready for prime-time.

jpvanoosten commented 4 years ago

I've created a project for this repo to give an indication of what is on the road-map.

marfier commented 4 years ago

The dev branch is like an entirely different project!

marfier commented 4 years ago

@jpvanoosten is there a chance that this could be closer to complete before the beginning of school season (September 1)?

jpvanoosten commented 4 years ago

@GalacticLion7 I won't be working on this project much before September. This project is currently being used in a production scenario so it is "complete" for our needs. If you'd like to see a live demo (you won't be able to log in however) check it out here: https://discord.igad.nl

marfier commented 4 years ago

@jpvanoosten I would like to make a Discord server that is only limited to users within a school, in the form of a G-Suite organization. Is this the right project for that? I just tried it out using the G-Suite method, and it seems that anybody who has a Google account could authenticate in the portal.

I would also like to personalize the portal, but I can't find all the site assets.

jpvanoosten commented 4 years ago

@GalacticLion7 If you want to limit it to students in your organization, make sure you check the box "Internal" under "Application type" in your in the OAuth consent screen: image

jpvanoosten commented 4 years ago

I would also like to personalize the portal, but I can't find all the site assets.

@GalacticLion7 The assets are in the /public folder and the PUG views (which generates the HTML) is in the /views folder. All images are in the /public/images folder. There you will find both the Adobe Illustrator (.ai) and PNG (.png) files that are used on the site. For example, just replace the public/images/brand_logo.svg file to use your own brand logo. If you want to change the file path to the brand logo, see: https://github.com/jpvanoosten/discord-user-manager/blob/968de0602a50d19bf4574cda80b4b7ee44bb1c13/views/nav.pug#L12

marfier commented 4 years ago

Thank you for clearing it up.

marfier commented 4 years ago

How about limiting invites to specific groups from G-Suite?

jpvanoosten commented 4 years ago

How about limiting invites to specific groups from G-Suite?

I don't think there is an automatic way to do this. I think you'd have to add the "https://www.googleapis.com/auth/admin.directory.group.readonly" scope to the GoogleStrategy in /routes/google.js (You'll also have to update your Google app project to allow this scope in the OAuth consent screen in the Developer Console). See: https://github.com/jpvanoosten/discord-user-manager/blob/566a0ab5e2baec9130ed3b984ee3d357607cebb6/routes/google.js#L66

Then, in the "verify" callback of the GoogleStrategy, you'd have to check the groups that the user is in. If the user isn't in the right group, then throw an error in the strategy.

See: https://developers.google.com/admin-sdk/directory/v1/quickstart/nodejs and https://developers.google.com/admin-sdk/directory/v1/guides/manage-groups#get_all_member_groups

I'm not 100% sure if this is correct advise, but I hope it gets you looking in the correct direction.

marfier commented 4 years ago

Aside from that, I'm getting a bunch of errors from the log: https://pastebin.com/1KwchWwh

Also, when I authenticated with my Google account, it displayed an error ("An error occured when adding you to the Discord server. Please contact the owner of the Discord server for more information"). I was able to finish Step 3 and it showed that my (alt) Discord account was linked, but I don't see the server on my Discord server list.

jpvanoosten commented 4 years ago

@GalacticLion7 The test scripts tries to add and remove the TEST_USER_DISCORD_ID user. Are you running the tests?

marfier commented 4 years ago

Nope, I left alone those env variables. I'm running it on production. On 7 Sep 2020, 03:05 +0400, Jeremiah van Oosten notifications@github.com, wrote:

@GalacticLion7 The test scripts tries to add and remove the TEST_DISCORD_ID user. Are you running the tests? — You are receiving this because you were mentioned. Reply to this email directly, view it on GitHub, or unsubscribe.

jpvanoosten commented 4 years ago

@GalacticLion7 The Sequelize debug output is clogging up the log output. Can you set the DEBUG environment variable to this:

DEBUG=discord-user-manager:*,-discord-user-manager:sequelize

and run the script again?

marfier commented 4 years ago

@jpvanoosten

yarn test --detectOpenHandles
yarn run v1.22.5
$ jest --detectOpenHandles
  console.error node_modules/jest-jasmine2/build/jasmine/Env.js:289
    Unhandled error

  console.error node_modules/jest-jasmine2/build/jasmine/Env.js:290
    DiscordAPIError: 404: Not Found
        at RequestHandler.execute (/srv/discord-user-manager/node_modules/discord.js/src/rest/RequestHandler.js:170:25)
        at processTicksAndRejections (internal/process/task_queues.js:93:5)

 FAIL  discord/DiscordAdapter.test.js
  ● Test suite failed to run

    Error: expect(received).not.toBeNull()

    Received: null

      79 | afterAll(async () => {
      80 |   const guildMember = DiscordAdapter.resolveGuildMember(process.env.TEST_USER_DISCORD_ID);
    > 81 |   expect(guildMember).not.toBeNull();
         |                           ^
      82 |   await DiscordAdapter.removeUser(guildMember);
      83 |   await DiscordAdapter.destroy();
      84 | });

      at Object.<anonymous> (discord/DiscordAdapter.test.js:81:27)

----------|----------|----------|----------|----------|-------------------|
File      |  % Stmts | % Branch |  % Funcs |  % Lines | Uncovered Line #s |
----------|----------|----------|----------|----------|-------------------|
All files |        0 |        0 |        0 |        0 |                   |
----------|----------|----------|----------|----------|-------------------|
Test Suites: 1 failed, 1 total
Tests:       0 total
Snapshots:   0 total
Time:        8.407s
Ran all test suites.
  console.error node_modules/jest-jasmine2/build/jasmine/Env.js:289
    Unhandled error

  console.error node_modules/jest-jasmine2/build/jasmine/Env.js:290
    DiscordAPIError: Caught error after test environment was torn down

    404: Not Found
        at RequestHandler.execute (/srv/discord-user-manager/node_modules/discord.js/src/rest/RequestHandler.js:170:25)
        at processTicksAndRejections (internal/process/task_queues.js:93:5)
yarn test
yarn run v1.22.5
$ jest
  console.error node_modules/jest-jasmine2/build/jasmine/Env.js:289
    Unhandled error

  console.error node_modules/jest-jasmine2/build/jasmine/Env.js:290
    DiscordAPIError: 404: Not Found
        at RequestHandler.execute (/srv/discord-user-manager/node_modules/discord.js/src/rest/RequestHandler.js:170:25)
        at processTicksAndRejections (internal/process/task_queues.js:93:5)

 FAIL  discord/DiscordAdapter.test.js
  ● Test suite failed to run

    Error: expect(received).not.toBeNull()

    Received: null

      79 | afterAll(async () => {
      80 |   const guildMember = DiscordAdapter.resolveGuildMember(process.env.TEST_USER_DISCORD_ID);
    > 81 |   expect(guildMember).not.toBeNull();
         |                           ^
      82 |   await DiscordAdapter.removeUser(guildMember);
      83 |   await DiscordAdapter.destroy();
      84 | });

      at Object.<anonymous> (discord/DiscordAdapter.test.js:81:27)

----------|----------|----------|----------|----------|-------------------|
File      |  % Stmts | % Branch |  % Funcs |  % Lines | Uncovered Line #s |
----------|----------|----------|----------|----------|-------------------|
All files |        0 |        0 |        0 |        0 |                   |
----------|----------|----------|----------|----------|-------------------|
Test Suites: 1 failed, 1 total
Tests:       0 total
Snapshots:   0 total
Time:        8.416s
Ran all test suites.
  console.error node_modules/jest-jasmine2/build/jasmine/Env.js:289
    Unhandled error

  console.error node_modules/jest-jasmine2/build/jasmine/Env.js:290
    DiscordAPIError: Caught error after test environment was torn down

    404: Not Found
        at RequestHandler.execute (/srv/discord-user-manager/node_modules/discord.js/src/rest/RequestHandler.js:170:25)
        at processTicksAndRejections (internal/process/task_queues.js:93:5)

Jest did not exit one second after the test run has completed.

This usually means that there are asynchronous operations that weren't stopped in your tests. Consider running Jest with `--detectOpenHandles` to troubleshoot this issue.
marfier commented 4 years ago

How about limiting invites to specific groups from G-Suite?

I don't think there is an automatic way to do this. I think you'd have to add the "https://www.googleapis.com/auth/admin.directory.group.readonly" scope to the GoogleStrategy in /routes/google.js (You'll also have to update your Google app project to allow this scope in the OAuth consent screen in the Developer Console). See:

https://github.com/jpvanoosten/discord-user-manager/blob/566a0ab5e2baec9130ed3b984ee3d357607cebb6/routes/google.js#L66

Then, in the "verify" callback of the GoogleStrategy, you'd have to check the groups that the user is in. If the user isn't in the right group, then throw an error in the strategy.

See: https://developers.google.com/admin-sdk/directory/v1/quickstart/nodejs and https://developers.google.com/admin-sdk/directory/v1/guides/manage-groups#get_all_member_groups

I'm not 100% sure if this is correct advise, but I hope it gets you looking in the correct direction.

Unfortunately, I'm not very much of a JS developer, but I did find this package: https://www.npmjs.com/package/google-group-manager.

jpvanoosten commented 4 years ago

So I started a new feature branch feature_restrict_google_group for this, but I didn't get very far yet.

@GalacticLion7 Maybe you want to take a look at the routes/google.js file to see if you can make any progress?

As far as I can see, it is likely that a service account will be required to query the groups on the domain.

jpvanoosten commented 4 years ago

Okay.. I think I have a basic idea about how to restrict user accounts by their group membership. I've added a test script that will list (at most 200) the email address of the groups that a particular user is a member of. See testGSuite.js. Change the hard-coded userKey field in that file to show group membership for another user.

There are a few things that you'll need to do to get this to work:

  1. Add the https://www.googleapis.com/auth/admin.directory.group.readonly scope to your web app in the Google Developer Console
  2. Get an OAuth access token for an admin account on your Google Apps domain (see the getToken function
  3. Add a function to check the groups for the user that wants to authenticate with the Discord User Manager and compare the group memberships with the allowed groups (not done).

This is still a work in progress and a massive struggle to get this to even remotely work as my first several attempts all failed miserably. Finding the right sample to solve it for this particular use case is like finding a needle in a haystack (or I was just focused on doing it in a particular way, which apparently wasn't supported....)

Please let me know if you're still interested in this functionality. If you are, I'll continue to try to make it work nicely for you (with a utility script to create the token file and add the functionality to the google login route).

jpvanoosten commented 4 years ago

Okay, checkout dced0c2f03fbc4adf4747b4c1cf20c30af89cd8b in the feature_restrict_google_group branch for an updated. I've added a utility script that can be used to get the OAuth2.0 token for your admin account. Use

npm run oauth:google

or

yarn oauth:google

to create the token.json file that is needed to query the user's groups. I've also added a function to list the user's groups (but currently doesn't do anything with them).

jpvanoosten commented 4 years ago

@GalacticLion7 Let me know if you need help with this...

marfier commented 4 years ago

@jpvanoosten the script seems to hang after being executed. I've let it run for ten minutes. Are there any logs we can see?

marfier commented 4 years ago

Also, in the latest feature_restrict_google_group, there's an error when I try to start the application.

root@discord-user-manager:/srv/discord-user-manager# yarn start
yarn run v1.22.5
$ node ./bin/www
/srv/discord-user-manager/node_modules/passport-oauth2/lib/strategy.js:86
  if (!options.clientID) { throw new TypeError('OAuth2Strategy requires a clientID option'); }
                           ^

TypeError: OAuth2Strategy requires a clientID option
    at Strategy.OAuth2Strategy (/srv/discord-user-manager/node_modules/passport-oauth2/lib/strategy.js:86:34)
    at new Strategy (/srv/discord-user-manager/node_modules/passport-azure-ad-oauth2/lib/passport-azure-ad-oauth2/strategy.js:58:18)
    at Object.<anonymous> (/srv/discord-user-manager/routes/azure.js:14:3)
    at Module._compile (internal/modules/cjs/loader.js:1076:30)
    at Object.Module._extensions..js (internal/modules/cjs/loader.js:1097:10)
    at Module.load (internal/modules/cjs/loader.js:941:32)
    at Function.Module._load (internal/modules/cjs/loader.js:782:14)
    at Module.require (internal/modules/cjs/loader.js:965:19)
    at require (internal/modules/cjs/helpers.js:88:18)
    at Object.<anonymous> (/srv/discord-user-manager/app.js:23:21)
    at Module._compile (internal/modules/cjs/loader.js:1076:30)
    at Object.Module._extensions..js (internal/modules/cjs/loader.js:1097:10)
    at Module.load (internal/modules/cjs/loader.js:941:32)
    at Function.Module._load (internal/modules/cjs/loader.js:782:14)
    at Module.require (internal/modules/cjs/loader.js:965:19)
    at require (internal/modules/cjs/helpers.js:88:18)
    at Object.<anonymous> (/srv/discord-user-manager/bin/www:8:13)
    at Module._compile (internal/modules/cjs/loader.js:1076:30)
    at Object.Module._extensions..js (internal/modules/cjs/loader.js:1097:10)
    at Module.load (internal/modules/cjs/loader.js:941:32)
    at Function.Module._load (internal/modules/cjs/loader.js:782:14)
    at Function.executeUserEntryPoint [as runMain] (internal/modules/run_main.js:72:12)
error Command failed with exit code 1.
info Visit https://yarnpkg.com/en/docs/cli/run for documentation about this command.
root@discord-user-manager:/srv/discord-user-manager# 
jpvanoosten commented 4 years ago

@GalacticLion7 I do need to spend some time on error handling. I've added a "Sign in with Microsoft" button and the routes/azure.js route to handle logging in with Azure AD. Perhaps this is due to the Azure AD client not being configured? I should look into wrapping those routes in checks if the CLIENT_ID is not configured for those services, not to include them ... So much to do, so little time...

Anyways, you marked your comments as resolved so maybe you figured it out already?

If you get the Google logins working, and you add a token file for your Google Admin user (using the utils/getGoogleOAuthToken.js script), then you should see a list of groups for the logged in user. The code to filter the user by the group membership still isn't there yet.

Another issues is when the access token expires, currently you'll need to generate a new one. I need to investigate how to refresh the access token if it's expired.

marfier commented 4 years ago

Anyways, you marked your comments as resolved so maybe you figured it out already?

It's because I overlooked your 4th last comment before I commented about my issues.

Get an OAuth access token for an admin account on your Google Apps domain

This sentence caught me, because I don't actually have access to an admin account. However, I am a user of the relevant groups (the ones I'm looking to whitelist), and I just want to confirm that I have read access to the users inside them by sending an API request (see HTTPie snippet below) to obtain the the list of users in a particular group I'm in.

I just need help to figure out where I can obtain an "access token" (that part of the Admin SDK documentation is too difficult to wrap my head around).

❯ http --json GET 'https://www.googleapis.com/admin/directory/v1/groups/2022@gemsdaa.net/members?key=AIzaSyDiN2xdVfEtXSB7fTunFa8i-3lg9SDxpTU' 'Authorization:Bearer [YOUR_ACCESS_TOKEN]'
HTTP/1.1 401 Unauthorized
Alt-Svc: h3-Q050=":443"; ma=2592000,h3-29=":443"; ma=2592000,h3-27=":443"; ma=2592000,h3-T051=":443"; ma=2592000,h3-T050=":443"; ma=2592000,h3-Q046=":443"; ma=2592000,h3-Q043=":443"; ma=2592000,quic=":443"; ma=2592000; v="46,43"
Content-Length: 507
Content-Type: application/json; charset=UTF-8
Date: Mon, 28 Sep 2020 20:07:06 GMT
Server: ESF
Vary: Origin
Vary: X-Origin
Vary: Referer
WWW-Authenticate: Bearer realm="https://accounts.google.com/", error="invalid_token"
X-Content-Type-Options: nosniff
X-Frame-Options: SAMEORIGIN
X-XSS-Protection: 0

{
    "error": {
        "code": 401,
        "errors": [
            {
                "domain": "global",
                "location": "Authorization",
                "locationType": "header",
                "message": "Invalid Credentials",
                "reason": "authError"
            }
        ],
        "message": "Request had invalid authentication credentials. Expected OAuth 2 access token, login cookie or other valid authentication credential. See https://developers.google.com/identity/sign-in/web/devconsole-project.",
        "status": "UNAUTHENTICATED"
    }
}

❯

If it turns out that I don't have read access to the groups, then unless there's a workaround, I would need to convince the system administrator to specially give my G-Suite account the relevant permissions. That may be difficult, since SA's are usually not developers, so the idea won't quite easily get to them.

jpvanoosten commented 4 years ago

I am a user of the relevant groups

This is not enough to query the groups of other users that try to login to your system. It's not even enough to read a list of your own groups!

The best solution is to request your Google Apps administrator to create a new admin role (called "Group Read" for example) that has only the Group > Read privilege under the "Admin API privileges" section (as shown in the screenshot below) and add you to that role.

image

You can test if this privilege is working by going to: https://developers.google.com/admin-sdk/directory/v1/reference/groups/list and scroll to the bottom of that page (replace the userKey value test@example.com with your own G Suite email address) and make sure only the https://www.googleapis.com/auth/admin.directory.group.readonly scope is checked (as shown in the screenshot below) and execute the query. You should be presented with an OAuth consent screen. Login using your own G Suite account and if you are able to see a list of groups for the specified in the userKey field, then the privilege is working.

image

I've setup a Discord server to continue this conversation: Join the Discord User Manager Discord Server I use this server for testing. The "logs" channel is used for testing log messages. Feel free to mute that channel!