diff --git a/app/components/billing/authorization.js b/app/components/billing/authorization.js index 39519fd4e9..0baa0c7b66 100644 --- a/app/components/billing/authorization.js +++ b/app/components/billing/authorization.js @@ -23,7 +23,6 @@ export default Component.extend({ requiresSource: equal('subscription.paymentIntent.status', 'requires_source'), lastPaymentIntentError: reads('subscription.paymentIntent.last_payment_error'), retryAuthorizationClientSecret: reads('subscription.paymentIntent.client_secret'), - hasSubscriptionPermissions: reads('account.hasSubscriptionPermissions'), notChargeInvoiceSubscription: not('subscription.chargeUnpaidInvoices.lastSuccessful.value'), freeV2Plan: equal('subscription.plan.startingPrice', 0), isSubscribed: reads('subscription.isSubscribed'), @@ -32,6 +31,10 @@ export default Component.extend({ canCancelSubscription: computed('isSubscribed', 'hasSubscriptionPermissions', 'freeV2Plan', 'isTrial', function () { return this.isSubscribed && this.hasSubscriptionPermissions && !this.freeV2Plan && !this.isTrial; }), + + hasSubscriptionPermissions: computed('account.hasSubscriptionPermissions', 'account.permissions', function () { + return this.account.hasSubscriptionPermissions && (!this.account.isOrganization || this.account.permissions.plan_create); + }), cancelSubscriptionLoading: reads('subscription.cancelSubscription.isRunning'), isTrial: reads('subscription.plan.isTrial'), isLoading: or('accounts.fetchSubscriptions.isRunning', 'accounts.fetchV2Subscriptions.isRunning', diff --git a/app/components/billing/payment-details-tab.js b/app/components/billing/payment-details-tab.js index 8128a620fb..f612c2db24 100644 --- a/app/components/billing/payment-details-tab.js +++ b/app/components/billing/payment-details-tab.js @@ -15,6 +15,8 @@ export default Component.extend({ metrics: service(), countries, + + model: reads('activeModel'), states: computed('country', function () { const { country } = this; @@ -37,6 +39,12 @@ export default Component.extend({ isV2SubscriptionEmpty: empty('v2subscription'), isSubscriptionEmpty: empty('v1subscription'), isSubscriptionsEmpty: and('isSubscriptionEmpty', 'isV2SubscriptionEmpty'), + canViewBilling: computed('model', function () { + return !this.account.isOrganization || this.account.permissions.billing_view; + }), + canEditBilling: computed('model', function () { + return !this.account.isOrganization || this.account.permissions.billing_update; + }), hasV2Subscription: not('isV2SubscriptionEmpty'), subscription: computed('v1subscription', 'v2subscription', function () { return this.isV2SubscriptionEmpty ? this.get('v1subscription') : this.get('v2subscription'); diff --git a/app/components/billing/select-plan.js b/app/components/billing/select-plan.js index d12358cd82..323f2cfe77 100644 --- a/app/components/billing/select-plan.js +++ b/app/components/billing/select-plan.js @@ -31,6 +31,9 @@ export default Component.extend({ return false; } }), + hasPlanChangePermission: computed('account', function () { + return !this.account.isOrganization || this.account.permissions.plan_create; + }), save: task(function* () { if (this.next.perform) { diff --git a/app/components/github-apps-repository.js b/app/components/github-apps-repository.js index dac327d63e..b025fa21a6 100644 --- a/app/components/github-apps-repository.js +++ b/app/components/github-apps-repository.js @@ -41,9 +41,18 @@ export default Component.extend({ return this.user && vcsLinks.accessSettingsUrl(this.user.vcsType, { owner: this.user.login }); }), + hasActivatePermission: computed('permissions.all', 'repository', function () { + let repo = this.repository; + let forRepo = (repo.owner.id == this.user.id && repo.ownerType == 'user') || + ((repo.shared || repo.ownerType != 'user') && repo.permissions.activate); + return forRepo; + }), + hasSettingsPermission: computed('permissions.all', 'repository', function () { let repo = this.repository; - return this.permissions.hasPushPermission(repo); + let forRepo = (repo.owner.id == this.user.id && repo.ownerType == 'user') || + ((repo.shared || repo.ownerType != 'user') && repo.permissions.settings_read); + return this.permissions.hasPushPermission(repo) && forRepo; }), hasEmailSubscription: computed('repository', 'repository.emailSubscribed', function () { diff --git a/app/components/log-content.js b/app/components/log-content.js index c72db2c278..cc103e3dbd 100644 --- a/app/components/log-content.js +++ b/app/components/log-content.js @@ -220,12 +220,13 @@ export default Component.extend({ return this.permissions.hasPermission(repo); }), - canRemoveLog: computed('job', 'job.canRemoveLog', 'hasPermission', function () { + canRemoveLog: computed('job', 'job.canRemoveLog', 'hasPermission', 'currentUser', function () { let job = this.job; let canRemoveLog = this.get('job.canRemoveLog'); let hasPermission = this.hasPermission; + let access = this.currentUser && this.currentUser.hasPermissionToRepo(this.get('job.repo'), 'log_delete'); if (job) { - return canRemoveLog && hasPermission; + return canRemoveLog && hasPermission && access; } }), @@ -247,7 +248,14 @@ export default Component.extend({ }, toggleLog() { - this.toggleProperty('logIsVisible'); + let access = this.currentUser && this.currentUser.hasPermissionToRepo(this.get('job.repo'), 'log_view'); + if (access) { + this.toggleProperty('logIsVisible'); + } else { + if (this.logIsVisible) { + this.toggleProperty('logIsVisible'); + } + } }, toggleRemoveLogModal() { diff --git a/app/components/profile-nav.js b/app/components/profile-nav.js index c0e2c13c7d..8ab652f27c 100644 --- a/app/components/profile-nav.js +++ b/app/components/profile-nav.js @@ -64,23 +64,47 @@ export default Component.extend({ isOrganization: reads('model.isOrganization'), hasAdminPermissions: reads('model.permissions.admin'), + hasPlanViewPermissions: reads('model.permissions.plan_view'), + hasPlanUsagePermissions: reads('model.permissions.plan_usage'), + hasPlanCreatePermissions: reads('model.permissions.plan_create'), + hasBillingViewPermissions: reads('model.permissions.billing_view'), + hasInvoicesViewPermissions: reads('model.permissions.plan_invoices'), + hasSettingsReadPermissions: reads('model.permissions.settings_read'), + hasSettingsCreatePermissions: reads('model.permissions.settings_create'), isOrganizationAdmin: and('isOrganization', 'hasAdminPermissions'), - showOrganizationSettings: and('isOrganizationAdmin', 'isProVersion'), - - showSubscriptionTab: computed('features.enterpriseVersion', 'model.isAssembla', 'model.isUser', function () { - const isAssemblaUser = this.model.isUser && this.model.isAssembla; - const isEnterprise = this.features.get('enterpriseVersion'); - return !isEnterprise && !isAssemblaUser && !!billingEndpoint; + showOrganizationSettings: computed('isOrganizationAdmin', 'isProVersion', 'hasSettingsCreatePermissions', function () { + const forOrganization = !this.isOrganization || this.hasSettingsCreatePermissions; + return this.isProVersion && forOrganization; }), - showPaymentDetailsTab: computed('showSubscriptionTab', 'isOrganization', 'isOrganizationAdmin', 'model.isNotGithubOrManual', function () { - if (this.isOrganization) { - return this.showSubscriptionTab && this.isOrganizationAdmin && this.model.get('isNotGithubOrManual'); - } else { - return this.showSubscriptionTab && this.model.get('isNotGithubOrManual'); - } + + showSubscriptionTab: computed('features.enterpriseVersion', 'hasPlanViewPermissions', + 'hasPlanCreatePermissions', 'model.isAssembla', 'model.isUser', + 'isOrganization', function () { + const forOrganization = !this.isOrganization || + ((this.model.hasSubscription || this.model.hasV2Subscription) && !!this.hasPlanViewPermissions) || + !!this.hasPlanCreatePermissions; + + const isAssemblaUser = this.model.isUser && this.model.isAssembla; + const isEnterprise = this.features.get('enterpriseVersion'); + return !isEnterprise && !isAssemblaUser && !!billingEndpoint && !!forOrganization; + }), + showPaymentDetailsTab: computed('showSubscriptionTab', 'isOrganization', 'isOrganizationAdmin', + 'hasBillingViewPermissions', 'hasInvoicesViewPermissions', 'model.isNotGithubOrManual', function () { + if (this.isOrganization) { + const forOrganization = !this.isOrganization || this.hasBillingViewPermissions || this.hasInvoicesViewPermissions; + + return this.showSubscriptionTab && this.model.get('isNotGithubOrManual') && (this.isOrganizationAdmin || forOrganization); + } else { + return this.showSubscriptionTab && this.model.get('isNotGithubOrManual'); + } + }), + showPlanUsageTab: computed('showSubscriptionTab', 'model.hasCredits', 'hasPlanUsagePermissions', function () { + const forOrganization = !this.isOrganization || this.hasPlanUsagePermissions; + return this.showSubscriptionTab && this.model.hasCredits && forOrganization; }), - showPlanUsageTab: and('showSubscriptionTab', 'model.hasCredits'), - usersUsage: computed('account.allowance.userUsage', 'addonUsage', function () { + + usersUsage: computed('account.allowance.userUsage', 'addonUsage', 'hasPlanUsagePermissions', function () { + // const forOrganization = !this.isOrganization || this.hasPlanUsagePermissions; const userUsage = this.model.allowance.get('userUsage'); if (userUsage === undefined) { return true; diff --git a/app/components/repo-actions.js b/app/components/repo-actions.js index 586e45c753..2a8035111d 100644 --- a/app/components/repo-actions.js +++ b/app/components/repo-actions.js @@ -38,6 +38,7 @@ export default Component.extend({ userHasPermissionForRepo: computed('repo.id', 'user', 'user.permissions.[]', function () { let repo = this.repo; let user = this.user; + if (user && repo) { return user.hasAccessToRepo(repo); } @@ -58,6 +59,27 @@ export default Component.extend({ return user.hasPushAccessToRepo(repo); } }), + userHasCancelPermissionForRepo: computed('repo.id', 'user', function () { + let repo = this.repo; + let user = this.user; + if (user && repo) { + return user.hasPermissionToRepo(repo, 'build_cancel'); + } + }), + userHasRestartPermissionForRepo: computed('repo.id', 'user', function () { + let repo = this.repo; + let user = this.user; + if (user && repo) { + return user.hasPermissionToRepo(repo, 'build_restart'); + } + }), + userHasDebugPermissionForRepo: computed('repo.id', 'user', function () { + let repo = this.repo; + let user = this.user; + if (user && repo) { + return user.hasPermissionToRepo(repo, 'build_debug'); + } + }), canOwnerBuild: reads('repo.canOwnerBuild'), ownerRoMode: reads('repo.owner.ro_mode'), @@ -68,9 +90,9 @@ export default Component.extend({ showPriority: true, showPrioritizeBuildModal: false, - canCancel: and('userHasPullPermissionForRepo', 'item.canCancel'), - canRestart: and('userHasPullPermissionForRepo', 'item.canRestart'), - canDebug: and('userHasPushPermissionForRepo', 'item.canDebug'), + canCancel: and('userHasCancelPermissionForRepo', 'item.canCancel'), + canRestart: and('userHasRestartPermissionForRepo', 'item.canRestart'), + canDebug: and('userHasDebugPermissionForRepo', 'item.canDebug'), isHighPriority: or('item.priority', 'item.build.priority'), isNotAlreadyHighPriority: not('isHighPriority'), hasPrioritizePermission: or('item.permissions.prioritize', 'item.build.permissions.prioritize'), diff --git a/app/components/repo-show-tools.js b/app/components/repo-show-tools.js index 9272a08d4c..46dfe30660 100644 --- a/app/components/repo-show-tools.js +++ b/app/components/repo-show-tools.js @@ -27,12 +27,16 @@ export default Component.extend({ displaySettingsLink: computed('permissions.all', 'repo', function () { let repo = this.repo; - return this.permissions.hasPushPermission(repo); + const forRepo = repo.permissions.settings_read; + + return this.permissions.hasPushPermission(repo) && forRepo; }), displayCachesLink: computed('permissions.all', 'repo', function () { let repo = this.repo; - return this.permissions.hasPushPermission(repo) && config.endpoints.caches; + const forRepo = repo.permissions.cache_view; + + return this.permissions.hasPushPermission(repo) && config.endpoints.caches && forRepo; }), displayStatusImages: computed('permissions.all', 'repo', function () { @@ -49,11 +53,12 @@ export default Component.extend({ let canTriggerBuild = this.get('repo.permissions.create_request'); let enterprise = this.get('features.enterpriseVersion'); let pro = this.get('features.proVersion'); + const forRepo = this.repo.permissions.build_create; if (enterprise || pro) { - return canTriggerBuild; + return canTriggerBuild && forRepo; } - return canTriggerBuild && migrationStatus !== 'migrated'; + return canTriggerBuild && migrationStatus !== 'migrated' && forRepo; } ), diff --git a/app/models/build.js b/app/models/build.js index b52e616ca3..56ab1bd34a 100644 --- a/app/models/build.js +++ b/app/models/build.js @@ -134,7 +134,9 @@ export default Model.extend(DurationCalculations, { return !isEmpty(jobs.filterBy('canCancel')); }), - canRestart: alias('isFinished'), + canRestart: computed('isFinished', function () { + return this.isFinished; + }), cancel() { const url = `/build/${this.id}/cancel`; diff --git a/app/models/job.js b/app/models/job.js index 4d07b6fe1e..9163139b90 100644 --- a/app/models/job.js +++ b/app/models/job.js @@ -2,7 +2,7 @@ import Model, { attr, belongsTo } from '@ember-data/model'; import { observer, computed } from '@ember/object'; -import { alias, and, equal, not, reads } from '@ember/object/computed'; +import { alias, and, equal, reads } from '@ember/object/computed'; import { inject as service } from '@ember/service'; import { isEqual } from '@ember/utils'; import { getOwner } from '@ember/application'; @@ -154,11 +154,13 @@ export default Model.extend(DurationCalculations, DurationAttributes, { canCancel: computed('isFinished', 'state', function () { let isFinished = this.isFinished; let state = this.state; - // not(isFinished) is insufficient since it will be true when state is undefined. return !isFinished && !!state; }), - canRestart: alias('isFinished'), + canRestart: computed('isFinished', function () { + let isFinished = this.isFinished; + return isFinished; + }), canDebug: and('isFinished', 'repo.private'), cancel() { @@ -234,7 +236,10 @@ export default Model.extend(DurationCalculations, DurationAttributes, { } }), - canRemoveLog: not('log.removed'), + canRemoveLog: computed('log.removed', function () { + let removed = !!this.log.removed; + return !removed; + }), slug: computed('repo.slug', 'number', function () { let slug = this.get('repo.slug'); diff --git a/app/models/repo.js b/app/models/repo.js index 41bb6d98f2..42b2ca6c3e 100644 --- a/app/models/repo.js +++ b/app/models/repo.js @@ -211,6 +211,9 @@ const Repo = VcsEntity.extend({ fetchSettings: task(function* () { if (!this.auth.signedIn) return {}; + + const hasPermissions = this.permissions.settings_read; + if (hasPermissions === false) return {}; try { const response = yield this.api.get(`/repo/${this.id}/settings`); return this._convertV3SettingsToV2(response.settings); diff --git a/app/models/user.js b/app/models/user.js index 91c7f31402..fb80baff66 100644 --- a/app/models/user.js +++ b/app/models/user.js @@ -73,6 +73,13 @@ export default Owner.extend({ } }, + hasPermissionToRepo(repo, permission) { + let permissions = repo.get ? repo.get('permissions') : null; + if (permissions) { + return permissions[permission] || false; + } + }, + sync(isOrganization) { this.set('isSyncing', true); this.set('applyFilterRepos', !isOrganization); diff --git a/app/routes/caches.js b/app/routes/caches.js index 7e08519180..ca0a45d433 100644 --- a/app/routes/caches.js +++ b/app/routes/caches.js @@ -11,6 +11,15 @@ export default TravisRoute.extend({ return this.controllerFor('repo').activate('caches'); }, + beforeModel() { + const repo = this.modelFor('repo'); + if (!repo.permissions.cache_view) { + this.transitionTo('repo.index'); + this.flashes.error('Your permissions are insufficient to access this repository\'s cache'); + } + }, + + model() { const repo = this.modelFor('repo'); const url = `/repo/${repo.get('id')}/caches`; diff --git a/app/routes/organization/billing.js b/app/routes/organization/billing.js index a28a8682bb..30a1ee308a 100644 --- a/app/routes/organization/billing.js +++ b/app/routes/organization/billing.js @@ -5,7 +5,7 @@ import AccountBillingMixin from 'travis/mixins/route/account/billing'; export default TravisRoute.extend(AccountBillingMixin, { model() { const organization = this.modelFor('organization'); - if (organization.permissions && organization.permissions.admin !== true) { + if (organization.permissions && organization.permissions.plan_view !== true) { this.transitionTo('organization.repositories', organization); } return hash({ diff --git a/app/routes/organization/plan_usage.js b/app/routes/organization/plan_usage.js index fcd1a4b612..9d99faadc2 100644 --- a/app/routes/organization/plan_usage.js +++ b/app/routes/organization/plan_usage.js @@ -5,7 +5,7 @@ import { hash } from 'rsvp'; export default TravisRoute.extend(AccountPlanUsageMixin, { model() { const organization = this.modelFor('organization'); - if (organization.permissions && organization.permissions.admin !== true) { + if (organization.permissions && organization.permissions.plan_usage !== true) { this.transitionTo('organization.repositories', organization); } return hash({ diff --git a/app/routes/organization/settings.js b/app/routes/organization/settings.js index 6083988bcd..c064bb0748 100644 --- a/app/routes/organization/settings.js +++ b/app/routes/organization/settings.js @@ -13,9 +13,6 @@ export default TravisRoute.extend({ model() { const organization = this.modelFor('organization'); - if (organization.permissions.admin !== true) { - this.transitionTo('organization.repositories', organization); - } const preferences = this.store.query('preference', { organization_id: organization.id }); return hash({ organization, preferences }); }, diff --git a/app/routes/settings.js b/app/routes/settings.js index bc781cfb1a..bcdb913827 100644 --- a/app/routes/settings.js +++ b/app/routes/settings.js @@ -67,8 +67,7 @@ export default TravisRoute.extend({ beforeModel() { const repo = this.modelFor('repo'); - const hasPushPermission = this.permissions.hasPushPermission(repo); - if (!hasPushPermission) { + if (!repo.permissions.settings_read) { this.transitionTo('repo.index'); this.flashes.error('Your permissions are insufficient to access this repository\'s settings'); } diff --git a/app/templates/components/billing/authorization.hbs b/app/templates/components/billing/authorization.hbs index 802962c5fa..8614a179a9 100644 --- a/app/templates/components/billing/authorization.hbs +++ b/app/templates/components/billing/authorization.hbs @@ -47,6 +47,8 @@ {{else if (and this.isComplete (or this.subscription.isStripe this.subscription.isManual))}} {{#if (or this.showPlansSelector this.showAddonsSelector)}} + + {{#if this.hasSubscriptionPermissions }} {{#if (not this.isV2Subscription)}} {{/if}} + {{/if}} {{else}} + {{#if this.hasSubscriptionPermissions }}
{{#if this.isLoading}} @@ -101,6 +105,7 @@
{{/if}} + {{/if}} {{/if}} {{/if}} -
+
@@ -158,7 +158,7 @@ {{#unless this.isTrial}}

{{format-currency this.selectedPlan.startingPrice floor="true"}}{{ if this.selectedPlan.isAnnual '/annualy' '/monthly'}}

- + {{#if this.planDetailsVisible}} @@ -174,7 +174,7 @@

Linux, Windows, macOS, FreeBSD

- + {{#if this.selectedPlan.hasOSSCreditAddons}}

{{this.selectedPlan.publicCredits}} OSS Credits {{#if this.isTrial}} We will charge you $1 and refund you in 7 days. This is needed to make sure your card - + is valid. By clicking on "Verify Your Account" you agree to Travis CI Terms and Privacy Policy. @@ -227,10 +227,10 @@ will not be able to use Travis CI features. {{else}} - + You'll be charged {{format-currency this.selectedPlan.startingPrice floor="true"}} {{ if this.selectedPlan.isAnnual 'annualy' 'monthly'}} until you cancel your subscription. Previous - + charges won't be refunded when you cancel unless it's legally required. By clicking on "{{this.getActivateButtonText}}" you agree to Travis CI Terms and Privacy Policy. diff --git a/app/templates/components/billing/payment-details-tab.hbs b/app/templates/components/billing/payment-details-tab.hbs index 51bdef1643..f62b1392fa 100644 --- a/app/templates/components/billing/payment-details-tab.hbs +++ b/app/templates/components/billing/payment-details-tab.hbs @@ -5,6 +5,7 @@ by placing test fee - $1, which will be returned to you within a week.

+ {{#if this.canViewBilling }}
@@ -49,7 +50,7 @@

{{stripeError.message}}

{{/if}} - + - +
 
- + - +
{{/if}}
- + {{#if this.showNonZeroVatConfirmation}}
@@ -211,6 +212,7 @@
{{/if}} + {{#if this.canEditBilling }}
{{#if this.isLoading}} @@ -221,9 +223,10 @@ {{/if}}
+ {{/if}}
- + {{/if}} {{#if this.invoices}} {{#if this.hasV2Subscription}} {{#if this.v2subscription.isNotManual}} diff --git a/app/templates/components/github-apps-repository.hbs b/app/templates/components/github-apps-repository.hbs index 4eb8fc3e2e..40835e87cb 100644 --- a/app/templates/components/github-apps-repository.hbs +++ b/app/templates/components/github-apps-repository.hbs @@ -21,13 +21,15 @@ {{/if}} -{{#if this.hasSettingsPermission}} +{{#if this.hasActivatePermission}} {{#if this.isNotMatchGithub}} {{/if}} +{{/if}} +{{#if this.hasSettingsPermission}} - + /> @@ -181,7 +181,7 @@ {{/if}} - {{#if (and this.showPlanUsageTab this.isOrganizationAdmin)}} + {{#if this.showPlanUsageTab}}
  • Plan usage diff --git a/app/templates/components/repo-actions.hbs b/app/templates/components/repo-actions.hbs index dfb1d6ea2e..3da5b803e6 100644 --- a/app/templates/components/repo-actions.hbs +++ b/app/templates/components/repo-actions.hbs @@ -124,10 +124,6 @@
  • {{/if}} {{/if}} -{{else}} - {{#if this.userHasPullPermissionForRepo}} - - {{/if}} {{/if}}