Skip to content

Commit

Permalink
feat: added Ubuntu One SSO demo (WIP untested)
Browse files Browse the repository at this point in the history
  • Loading branch information
titanism committed Apr 23, 2024
1 parent c7294e7 commit fd2abc3
Show file tree
Hide file tree
Showing 52 changed files with 2,079 additions and 6,880 deletions.
4 changes: 4 additions & 0 deletions .env.defaults
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,10 @@ AUTH_GOOGLE_ENABLED=false
AUTH_GITHUB_ENABLED=false
AUTH_OTP_ENABLED=false
AUTH_WEBAUTHN_ENABLED=true
AUTH_UBUNTU_ENABLED=false
# ubuntu
UBUNTU_CALLBACK_URL={{{WEB_URL}}}/auth/ubuntu/ok
UBUNTU_REALM={{{WEB_URL}}}
# your sign-in with apple configuration
# https://github.com/nicokaiser/passport-apple#create-a-service
APPLE_CLIENT_ID=
Expand Down
4 changes: 4 additions & 0 deletions .env.schema
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,10 @@ AUTH_GOOGLE_ENABLED=
AUTH_GITHUB_ENABLED=
AUTH_OTP_ENABLED=
AUTH_WEBAUTHN_ENABLED=
AUTH_UBUNTU_ENABLED=
# ubuntu
UBUNTU_CALLBACK_URL=
UBUNTU_REALM=
# your sign-in with apple configuration
# https://github.com/nicokaiser/passport-apple#create-a-service
APPLE_CLIENT_ID=
Expand Down
16 changes: 16 additions & 0 deletions app/controllers/web/my-account/create-alias.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,22 @@ async function createAlias(ctx, next) {
Boom.badRequest(ctx.translateError('ALIAS_WITH_PLUS_UNSUPPORTED'))
);

//
// if the domain is ubuntu.com and the user is in the user group
// then don't allow them to create aliases (only manage/delete their own)
//
if (ctx.state.domain.name === 'ubuntu.com') {
const member = ctx.state.domain.members.find(
(member) => member.user && member.user.id === ctx.state.user.id
);

if (!member)
return ctx.throw(Boom.notFound(ctx.translateError('INVALID_USER')));

if (member.group === 'user')
return ctx.throw(Boom.notFound(ctx.translateError('UBUNTU_PERMISSIONS')));
}

try {
ctx.state.alias = await Aliases.create({
...ctx.state.body,
Expand Down
20 changes: 20 additions & 0 deletions app/controllers/web/my-account/validate-alias.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
* SPDX-License-Identifier: BUSL-1.1
*/

const Boom = require('@hapi/boom');
const _ = require('lodash');
const isSANB = require('is-string-and-not-blank');
const slug = require('speakingurl');
Expand Down Expand Up @@ -101,6 +102,25 @@ function validateAlias(ctx, next) {
if (ctx.api && _.isEmpty(body.recipients))
body.recipients = [ctx.state.user.email];

//
// if the domain is ubuntu.com and the user is in the user group
// then don't allow them to enable IMAP
//
if (ctx.state.domain.name === 'ubuntu.com') {
const member = ctx.state.domain.members.find(
(member) => member.user && member.user.id === ctx.state.user.id
);

if (!member)
return ctx.throw(Boom.notFound(ctx.translateError('INVALID_USER')));

if (member.group === 'user' && body.has_imap)
return ctx.throw(Boom.notFound(ctx.translateError('UBUNTU_PERMISSIONS')));

if (_.isArray(body.recipients) && body.recipients.some(r => isEmail(r) && r.endsWith('@ubuntu.com')))
return ctx.throw(Boom.notFound(ctx.translateError('UBUNTU_NOT_ALLOWED_EMAIL')));
}

ctx.state.body = body;

return next();
Expand Down
178 changes: 178 additions & 0 deletions app/models/users.js
Original file line number Diff line number Diff line change
Expand Up @@ -398,6 +398,15 @@ object[fields.githubProfileID] = {
};
object[fields.githubAccessToken] = String;
object[fields.githubRefreshToken] = String;
// ubuntu
object[fields.ubuntuProfileID] = {
type: String,
index: true
};
object[fields.ubuntuUsername] = {
type: String,
index: true
};

object[fields.otpEnabled] = {
type: Boolean,
Expand Down Expand Up @@ -888,6 +897,175 @@ Users.pre('save', function (next) {
next();
});

//
// when ubuntu users sign in we need to check their membership
// (and probably need some automated job that checks this for active memberships)
//
Users.pre('save', async function (next) {
if (!isSANB(this[fields.ubuntuProfileID])) return next();
try {
if (!isSANB(this[fields.ubuntuUsername])) {
const error = Boom.badRequest(
i18n.api.t({
phrase: config.i18n.phrases.UBUNTU_INVAILD_USERNAME,
locale: this[config.lastLocaleField]
})
);
error.no_translate = true;
throw error;
}

const response = await retryRequest(
`https://api.launchpad.net/1.0/~${
this[fields.ubuntuUsername]
}/memberships_details`
);

const json = await response.body.json();

if (
!_.isObject(json) ||
!_.isNumber(json.start) ||
!_.isNumber(json.total_size) ||
!_.isArray(json.entries)
) {
const error = Boom.badRequest(
i18n.api.t({
phrase: config.i18n.phrases.UBUNTU_API_RESPONSE_INVALID,
locale: this[config.lastLocaleField]
})
);
error.no_translate = true;
throw error;
}

// TODO: support pagination for users that have paginated memberships

if (
_.isEmpty(json.entries) ||
json.total_size === 0 ||
!json.entries.some(
(entry) =>
entry.team_link === 'https://api.launchpad.net/1.0/~ubuntumembers'
)
) {
const error = Boom.badRequest(
i18n.api.t({
phrase: config.i18n.phrases.UBUNTU_INVALID_GROUP,
locale: this[config.lastLocaleField]
})
);
error.no_translate = true;
throw error;
}

/*
{
"start": 0,
"total_size": 5,
"entries": [
{
"self_link": "https://api.launchpad.net/1.0/~some-team/+member/nickname",
"web_link": "https://launchpad.net/~some-team/+member/nickname",
"resource_type_link": "https://api.launchpad.net/1.0/#team_membership",
"team_link": "https://api.launchpad.net/1.0/~some-team",
"member_link": "https://api.launchpad.net/1.0/~nickname",
"last_changed_by_link": "https://api.launchpad.net/1.0/~some-nickname",
"date_joined": "2016-09-19T08:24:53.878044+00:00",
"date_expires": null,
"last_change_comment": "some comment",
"status": "Administrator",
"http_etag": "\"some-etag\""
},
...
],
"resource_type_link": "https://api.launchpad.net/1.0/#team_membership-page-resource"
}
*/

//
// now we need to find the @ubuntu.com domain
// and create the user their alias if not already exists
//
const adminIds = await this.constructor.distinct('_id', {
group: 'admin'
});

const domain = await conn.models.Domains.findOne({
name: 'ubuntu.com',
plan: 'team',
'members.user': {
$in: adminIds
}
})
.lean()
.exec();

if (!domain) {
const error = Boom.badRequest(
i18n.api.t({
phrase: config.i18n.phrases.DOMAIN_DOES_NOT_EXIST_ANYWHERE,
locale: this[config.lastLocaleField]
})
);
error.no_translate = true;
throw error;
}

//
// otherwise check that the domain includes this user id
// and if not, then add it to the group as a user
//
const match = domain.members.find(
(m) => m.user.toString() === this._id.toString()
);
if (!match) {
domain.members.push({
user: this._id,
group: 'user'
});
await domain.save();
}

// now check that if the alias already exists and is owned by this user
const alias = await conn.models.Aliases.findOne({
user: this._id,
domain: domain._id,
name: this[fields.ubuntuUsername].toLowerCase()
});

// if not, then create it, but only if there aren't already 3+ aliases owned by this user
if (!alias) {
const count = await conn.models.Aliases.countDocuments({
user: this._id,
domain: domain._id
});
if (count > 3) {
const error = Boom.badRequest(
i18n.api.t({
phrase: config.i18n.phrases.UBUNTU_MAX_LIMIT,
locale: this[config.lastLocaleField]
})
);
error.no_translate = true;
throw error;
}

await conn.models.Aliases.create({
user: this._id,
domain: domain._id,
name: this[fields.ubuntuUsername].toLowerCase(),
recipients: [this.email],
locale: this[config.lastLocaleField]
});
}

next();
} catch (err) {
next(err);
}
});

Users.postCreate((user, next) => {
logger.info('user created', {
user: user.toObject()
Expand Down
21 changes: 11 additions & 10 deletions app/views/_breadcrumbs.pug
Original file line number Diff line number Diff line change
Expand Up @@ -108,16 +108,17 @@ if breadcrumbs.length > 1
!= t('Import <span class="notranslate">TXT</span> Records')
= " "
i.fa.fa-cloud-download-alt
li.list-inline-item.mb-1
a.btn.btn-dark(
href=l(`/my-account/domains/${domain.name}/aliases/new`),
role="button",
data-toggle="modal-anchor",
data-target="#modal-alias"
)
= t("Add Alias")
= " "
i.fa.fa-angle-double-right.align-middle
if domain.name !== 'ubuntu.com' || domain.group === 'admin'
li.list-inline-item.mb-1
a.btn.btn-dark(
href=l(`/my-account/domains/${domain.name}/aliases/new`),
role="button",
data-toggle="modal-anchor",
data-target="#modal-alias"
)
= t("Add Alias")
= " "
i.fa.fa-angle-double-right.align-middle
else
case ctx.pathWithoutLocale
when '/my-account/logs'
Expand Down
4 changes: 2 additions & 2 deletions app/views/_search-form.pug
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ form.d-block.d-md-none(action=l("/search"), method="GET")
.input-group-prepend
span.input-group-text
i.fa.fa.fa-search
input#input-search.form-control.form-control-sm(
input#input-search-sm.form-control.form-control-sm(
type="search",
name="q",
value=ctx.query.q
Expand All @@ -15,7 +15,7 @@ form.d-none.d-md-block(action=l("/search"), method="GET")
.input-group-prepend
span.input-group-text
i.fa.fa.fa-search
input#input-search.form-control.form-control-lg(
input#input-search-md.form-control.form-control-lg(
type="search",
name="q",
value=ctx.query.q
Expand Down
50 changes: 34 additions & 16 deletions app/views/layout.pug
Original file line number Diff line number Diff line change
Expand Up @@ -56,22 +56,40 @@ html.h-100.no-js(
integrity=manifest("img/apple-touch-icon.png", "integrity"),
crossorigin="anonymous"
)
link(
rel="icon",
type="image/png",
href=manifest("img/favicon-32x32.png"),
sizes="32x32",
integrity=manifest("img/favicon-32x32.png", "integrity"),
crossorigin="anonymous"
)
link(
rel="icon",
type="image/png",
href=manifest("img/favicon-16x16.png"),
sizes="16x16",
integrity=manifest("img/favicon-16x16.png", "integrity"),
crossorigin="anonymous"
)
if ctx.pathWithoutLocale.startsWith('/ubuntu') || (domain && domain.name && domain.name === 'ubuntu.com')
link(
rel="icon",
type="image/png",
href=manifest("img/ubuntu-favicon-32x32.png"),
sizes="32x32",
integrity=manifest("img/ubuntu-favicon-32x32.png", "integrity"),
crossorigin="anonymous"
)
link(
rel="icon",
type="image/png",
href=manifest("img/ubuntu-favicon-16x16.png"),
sizes="16x16",
integrity=manifest("img/ubuntu-favicon-16x16.png", "integrity"),
crossorigin="anonymous"
)
else
link(
rel="icon",
type="image/png",
href=manifest("img/favicon-32x32.png"),
sizes="32x32",
integrity=manifest("img/favicon-32x32.png", "integrity"),
crossorigin="anonymous"
)
link(
rel="icon",
type="image/png",
href=manifest("img/favicon-16x16.png"),
sizes="16x16",
integrity=manifest("img/favicon-16x16.png", "integrity"),
crossorigin="anonymous"
)
//- href=manifest("site.webmanifest"),
//- integrity=manifest("site.webmanifest", "integrity"),
link(rel="manifest", href="/site.webmanifest", crossorigin="anonymous")
Expand Down
Loading

0 comments on commit fd2abc3

Please sign in to comment.