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

Invalidusernames command #919

Merged
merged 9 commits into from Nov 8, 2018
Merged

Invalidusernames command #919

merged 9 commits into from Nov 8, 2018

Conversation

megoth
Copy link
Contributor

@megoth megoth commented Nov 8, 2018

This is my attempt at fixing #824.

A lot of things happening here, but it boils down to a command with two options:

  • [solid] invalidusernames: Will list all invalid usernames that exists on the server today
  • [solid] invalidusernames --notify: Will go through all invalid accounts with invalid usernames and
    1. Make a backup of their existing index.html (move it to index.backup.html)
    2. Create a new index.html that contains info that this account will be deleted in two weeks (based on the new template default-views/account/invalid-username.hbs)
    3. Send an email (if available) with the same notification (based on the template new default-templates/emails/invalid-username.js)
  • [solid] invalidusernames --delete: Will delete all accounts with invalid usernames

I've also added a new option in the init script that prompts for a support-email for POD-providers (this will only show if the multiuser-property is set to true). As part of this I made use of the isEmail-method in https://www.npmjs.com/package/validator, which I understand from #916 might not've been the most well-thought through decision.

I've tried writing some tests, but find it difficult to write tests for a lot of this =/

Any and all feedback are (as always) appreciated, and I hope I'm not too far off with this solution.

@RubenVerborgh
Copy link
Contributor

Why is there both a invalid-username.hbs and .js?

@megoth
Copy link
Contributor Author

megoth commented Nov 8, 2018

  • invalid-username.hbs is for generating the index.html that replaces the (backed up) index.html in a user's POD
  • invalid-username.js is for generating the email that is sent (if email address is available)

const { cyan, bold } = require('colorette')
const util = require('util')

module.exports = {}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I could be wrong, but isn't this the default behaviour in CommonJS?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, you're right, it's unnecessary. I'll remove it.

})
} catch (err) {
// No file exists, not a problem
console.log(cyan(bold('TIP')), 'create a config.json: `$ solid init`')
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Think about using a logger library (likely a new issue). My favourite is loglevel (because it's simple and supports plugins).

winston is popular also.

This way you can fine-tune the logging.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Definitely a new issue, the required changes would be server-wide.
That said, it /should/ eventually be done, since one of the most frequently requested features about logging is timestamps, which our current logger does not support.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Logging is great for long-running processes (and the server has logging), but this is just a short one-time job.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, actually, I should probably reuse debug that's laid out via lib/debug.js.

Copy link
Contributor Author

@megoth megoth Nov 8, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We might consider a logger too, but yeah, that should be its own issue.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

let configFile = argv['configFile'] || './config.json'

try {
const file = await util.promisify(fs.readFile)(configFile)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is that the only occurence of fs?
If so, would const { readFile } = require('fs') work also?

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Plus const { promisify } = require('util').

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After Node 8, I'm pretty sure all fs methods have a Promise interface natively, so the promisify is not even needed.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

https://nodejs.org/api/fs.html#fs_fs_promises_api currently has stability: experimental.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

currently has stability: experimental.

Ohhh right, I was thinking about fs-extra, never mind :)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, I'll write away promisify.

.option('--notify', 'Will notify users with usernames that are invalid')
.option('--delete', 'Will delete users with usernames that are invalid')
.description('Manage usernames that are invalid')
.action(async (options) => {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My personal view: I move the implementation of the action into another file / function.
This way I can have different interfaces for it (CLI, Web UI etc).
Plus, it is easier to test.
Plus, it is a separation of concerns (CLI vs. using the lib).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would do it 100% if I thought this code was reusable, which I'm leaning toward it isn't =/

As I mentioned earlier, I found it difficult to write tests for this. It relies on a lot of file-based functionality, which makes it unsuited for unit tests. I could write integration tests, but didn't see any for the other commands, so didn't know how to attack it.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

From an Angular point of view I'd argue with Dependency Injection.
That is, providing the fs as an argument to the function.
But it feels meh somehow.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not that familiar with dependency injection in Node. I've tried MockRequire earlier, but my tests always turned a nightmare to maintain.

}

const invalidUsernames = await getInvalidUsernames(config)
const host = SolidHost.from({ port: config.port, serverUri: config.serverUri })
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hm, I wonder, whether const host = SolidHost.from({ config }) would work.
Like in Unpacking fields from objects passed as function parameter

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I reckon you mean const host = SolidHost.from(config)? (As const host = SolidHost.from({ config }) is a shortcut for const host = SolidHost.from({ config: config }).)

Could work, but I wanted to stay specific reg this.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed, more specific is better in this case (since it's clearer which args are actually used)

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fair enough.

})
}

async function createNewIndexFile (username, accountManager, invalidUsernameTemplate, dateOfRemoval, supportEmail, fileOptions) {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Personally I don't like functions with so many arguments. I consider them a code smell.
Like the function trying to do too much.
It helps to have smaller functions, which are then called in order.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't like long signatures either, but in this case it's not really reusable functions, but I wanted more readable code by splitting it up into "logical" chunks of code (in what I hope is good named functions).

But yeah, if we wanted to reuse this functionality, I should probably restructure it a bit to decrease the length of the signatures.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well, if you are struggling with writing tests, this should be argument enough to break the function up, shouldn't it?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If I knew how to break up this code into reusable, testable functions, I would do it ;)

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay, I can write the high-level steps I see. Implementation goes into the functions then …

async function createNewIndexFile (/* arguments */) {
  if (isBackupNeeded(/* arguments */)) {
    await Promise.all([
      backupIndexHtml(/* arguments */)
      backupAcl(/* arguments */)
    ])
    await Promise.all([
      generateNewIndexHtml(/* arguments */)
      generateNewAcl(/* arguments */)
    ])
    logCompletion(/* arguments */)
  }
}

or maybe group the backup + creation into a single function (like an transaction).
You may want to timestamp the index.backup.html also.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Plus, looking at the function name, createNewIndexFile is misleading, since it does more than it says …
so its caller would need to call more functions.

.map(async username => {
try {
const user = accountManager.userAccountFrom({ username })
await oidcManager.users.deleteUser(user)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IIRC I've read in the docs, that other auth managers (than oidc) would be supported as well.
So I am wondering here, why it has to be oidc.
Maybe add an comment? Could be that it is a „stupid question” for contributors familiar with the code.

Copy link
Contributor Author

@megoth megoth Nov 8, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, it's not a stupid question; We support WebID-TLS and WebID-OIDC now, and WebID-TLS don't store user-data like WebID-OIDC does. (If I'm wrong, pleasepleaseplease correct me =P )

I could try to abstract away the OIDC-part of this, to accommodate future auth managers better, but I think that's going to might take a long time (for me, at least - I'm not that into auth managers yet). So I was thinking this can be done when we implement support for other auth managers.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe you could leave a comment here, so the next person touching the code knows your thoughts (since I'd not look up the associated PR immediately, but rather git blame …).

I don't know the conventions in this project, though.

const userDirectory = accountManager.accountDirFor(username)
const currentIndex = path.join(userDirectory, 'index.html')
const currentIndexExists = await fileExists(currentIndex)
const backupIndex = path.join(userDirectory, 'index.backup.html')
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is still in documentRoot, i.e. accessible from the Internet (for everybody?)?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, should probably copy the acl-file as well O_O

const isUserDirectory = new RegExp(`.${hostname}$`)
return files
.filter(file => isUserDirectory.test(file))
.map(userDirectory => userDirectory.substr(0, userDirectory.length - hostname.length - 1))
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why exactly userDirectory.length - hostname.length - 1?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Userdirectory can be read as [username].[hostname], so it's to remove the dot between username and hostname.

Copy link

@Ryuno-Ki Ryuno-Ki Nov 8, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it fixed that there must be a dot?
If so, can't you split the hostname (and take the second-to-last value)?

if (usernames.length === 0) {
console.info('No invalid usernames was found')
}
console.info(`${usernames.length} invalid usernames were found:${usernames.map(username => `\n- ${username}`)}`)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't that go into an else branch?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, forgot a return in the if-block (that's at least how I like to write that logic).

}

async function notifyUsers (usernames, accountManager, config) {
const twoWeeksFromNow = Date.now() + 14 * 24 * 60 * 60 * 1000
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you ever struggle with datetimes, look at date-fns or moment.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I prefer the former, but didn't want to include it just for this.

}))
const emailService = new EmailService(templates.email, config.email)
const sendingEmails = await users
.filter(user => !!user.emailAddress)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe more readable as .filter(user => Boolean(user.emailAddress))?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I disagree, !! is more standard, and is used throughout our libs.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

!! reminds me on the output of uglify :-)
But being used throughout is a valid argument.

}

async function updateIndexFiles (usernames, accountManager, dateOfRemoval, supportEmail) {
const invalidUsernameFilePath = path.join(process.cwd(), 'default-views/account/invalid-username.hbs')
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do you use path.join when the second argument is not OS-agnostic?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Was planning to see if I could find another a way to reference default-views/account, but forgot about it. I'll make another try, or at least fix this to be OS-agnostic.

*/
text: `Hi,

We're sorry to inform you that the username for account ${data.accountUri} is not allowed after changes to username policy.
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe refer to username policy somehow? I'd be wondering, what exactly is in there if I would receive such an e-mail.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this is necessary, but open to be convinced otherwise.

<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Invalid username</title>
<link rel="stylesheet" href="/common/css/bootstrap.min.css">
<script></script>
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Huh? An empty tag?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will remove.

</head>
<body>
<div class="container">
<h4>Invalid username</h4>
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not an h1? Semantic Web and so on.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because of legacy; the other views use h4 (I reckon they did it like this initially because they thought h1 was to big; We (mostly) use bootstrap (3.3.4) for the presentation).

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@megoth hehe, that's exactly why, h1 was too big on bootstrap :)

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Ryuno-Ki
Copy link

Ryuno-Ki commented Nov 8, 2018

I left some comments while walking through the files.

@kjetilk kjetilk merged commit cdaff9b into master Nov 8, 2018
@ghost ghost removed the in progress label Nov 8, 2018
@kjetilk
Copy link
Member

kjetilk commented Nov 8, 2018

Great with a lot of comments, I just wanted to note that this code will most likely run only once, so it is not worth spending too much time on it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

5 participants