| 1 | import type { INestApplication } from '@nestjs/common'; |
| 2 | import { AuthService } |
| 3 | import request from 'supertest |
| 4 | import { afterAll, beforeAll |
| 5 | |
| 6 | import type { Auth |
| 7 | import { hasBackends, signUp |
| 8 | |
| 9 | const GH = ' |
| 10 | |
| 11 | /** Organization OAuth app access restrictions: an app the organization has not approved reaches none of its repositories, over GraphQL, REST or git, while keys made in Ghost, apps built into it and the organization's own apps keep working. */ |
| 12 | describe.skipIf(!hasBackends |
| 13 | let app: |
| 14 | let owner: |
| 15 | let member: |
| 16 | let developer: |
| 17 | let thirdParty: |
| 18 | let organizationApp: |
| 19 | const stamp = |
| 20 | const org = |
| 21 | const api = |
| 22 | const auth = |
| 23 | /** A key as an OAuth app is given one, the way the token endpoint mints it. */ |
| 24 | const appKey = |
| 25 | ( |
| 26 | await auth().createApiKey |
| 27 | body: { |
| 28 | userId, |
| 29 | permissions: { scopes: [ |
| 30 | metadata: { oauthClientId: clientId |
| 31 | }, |
| 32 | }) |
| 33 | ).key; |
| 34 | const restrict = |
| 35 | api() |
| 36 | .patch(`/api/organizations/${org |
| 37 | .set('cookie', |
| 38 | .send({ oauthAppRestrictions: |
| 39 | .expect(200); |
| 40 | const rest = |
| 41 | api() |
| 42 | .get(`/api/v3/repos/${org |
| 43 | .set('authorization', |
| 44 | const graphql = |
| 45 | ( |
| 46 | await api() |
| 47 | .post('/api/graphql') |
| 48 | .set('authorization', |
| 49 | .send({ |
| 50 | query: `{ repository(owner: "${org |
| 51 | }) |
| 52 | .expect(200) |
| 53 | ).body; |
| 54 | const git = |
| 55 | api() |
| 56 | .get(`/${org |
| 57 | .set( |
| 58 | 'authorization', |
| 59 | `Basic ${Buffer.from |
| 60 | ); |
| 61 | |
| 62 | beforeAll(async () |
| 63 | ({ app } |
| 64 | owner = await |
| 65 | member = await |
| 66 | developer = await |
| 67 | const organizationId = |
| 68 | body: { name: ' |
| 69 | headers: new Headers |
| 70 | }))!.id; |
| 71 | await auth().addMember |
| 72 | body: { userId: member |
| 73 | }); |
| 74 | await api() |
| 75 | .post('/api/repositories') |
| 76 | .set('cookie', |
| 77 | .send({ name: |
| 78 | .expect(201); |
| 79 | const apps = |
| 80 | name: 'Tool', |
| 81 | homepageUrl: 'https://tool.example', |
| 82 | callbackUrls: ['https://tool.example/callback'], |
| 83 | }; |
| 84 | thirdParty = ( |
| 85 | await api() |
| 86 | .post('/api/oauth-apps') |
| 87 | .set('cookie', |
| 88 | .send(apps) |
| 89 | .expect(201) |
| 90 | ).body.clientId; |
| 91 | organizationApp = ( |
| 92 | await api() |
| 93 | .post(`/api/organizations/${org |
| 94 | .set('cookie', |
| 95 | .send(apps) |
| 96 | .expect(201) |
| 97 | ).body.clientId; |
| 98 | }); |
| 99 | |
| 100 | afterAll(async () |
| 101 | await app?.close |
| 102 | }); |
| 103 | |
| 104 | it('lets every app in while restrictions are off', |
| 105 | const key = |
| 106 | await rest(key |
| 107 | expect(await graphql |
| 108 | data: { repository: { |
| 109 | }); |
| 110 | await git(key |
| 111 | }); |
| 112 | |
| 113 | it('hides the repositories from an app the organization has not approved, everywhere', |
| 114 | await restrict(true) |
| 115 | const key = |
| 116 | await rest(key |
| 117 | expect((await graphql |
| 118 | await git(key |
| 119 | expect( |
| 120 | ( |
| 121 | await api() |
| 122 | .post(`/api/v3/orgs/${org |
| 123 | .set( |
| 124 | 'authorization', |
| 125 | `token ${await appKey |
| 126 | ) |
| 127 | .send({ name: |
| 128 | .expect(403) |
| 129 | ).body.message, |
| 130 | ).toContain('has enabled OAuth App access restrictions' |
| 131 | }); |
| 132 | |
| 133 | it('keeps keys made in Ghost, apps built into it, and its own apps working', |
| 134 | await rest(member |
| 135 | await rest(await |
| 136 | await rest(await |
| 137 | }); |
| 138 | |
| 139 | it('takes a member’s request, and an admin’s approval lets the app in until it is revoked', |
| 140 | const key = |
| 141 | const asked = |
| 142 | .post(`/api/organizations/${org |
| 143 | .set('cookie', |
| 144 | .expect(201); |
| 145 | expect(asked.body |
| 146 | clientId: thirdParty, |
| 147 | state: 'requested', |
| 148 | requestedBy: `oaaccessmember${stamp |
| 149 | decidedBy: null, |
| 150 | }); |
| 151 | await api() |
| 152 | .post(`/api/organizations/${org |
| 153 | .set('cookie', |
| 154 | .expect(404); |
| 155 | await api() |
| 156 | .get(`/api/organizations/${org |
| 157 | .set('cookie', |
| 158 | .expect(403); |
| 159 | await api() |
| 160 | .put(`/api/organizations/${org |
| 161 | .set('cookie', |
| 162 | .send({ state: |
| 163 | .expect(403); |
| 164 | |
| 165 | const approved = |
| 166 | .put(`/api/organizations/${org |
| 167 | .set('cookie', |
| 168 | .send({ state: |
| 169 | .expect(200); |
| 170 | expect(approved.body |
| 171 | state: 'approved', |
| 172 | requestedBy: `oaaccessmember${stamp |
| 173 | decidedBy: `oaaccessowner${stamp |
| 174 | }); |
| 175 | await rest(key |
| 176 | await git(key |
| 177 | const listed = |
| 178 | .get(`/api/organizations/${org |
| 179 | .set('cookie', |
| 180 | .expect(200); |
| 181 | expect(listed.body |
| 182 | restricted: true, |
| 183 | apps: [ |
| 184 | { |
| 185 | clientId: thirdParty, |
| 186 | name: 'Tool', |
| 187 | state: 'approved', |
| 188 | owner: `oaaccessdev${stamp |
| 189 | homepageUrl: 'https://tool.example', |
| 190 | scopes: ['read:org', |
| 191 | }, |
| 192 | ], |
| 193 | }); |
| 194 | |
| 195 | // asking again leaves the answer as it is |
| 196 | await api() |
| 197 | .post(`/api/organizations/${org |
| 198 | .set('cookie', |
| 199 | .expect(201) |
| 200 | .expect((response) |
| 201 | |
| 202 | await api() |
| 203 | .delete(`/api/organizations/${org |
| 204 | .set('cookie', |
| 205 | .expect(204); |
| 206 | await rest(key |
| 207 | |
| 208 | await api() |
| 209 | .put(`/api/organizations/${org |
| 210 | .set('cookie', |
| 211 | .send({ state: |
| 212 | .expect(200); |
| 213 | await rest(key |
| 214 | await api() |
| 215 | .put(`/api/organizations/${org |
| 216 | .set('cookie', |
| 217 | .send({ state: |
| 218 | .expect(404); |
| 219 | await api() |
| 220 | .put(`/api/organizations/${org |
| 221 | .set('cookie', |
| 222 | .send({ state: |
| 223 | .expect(400); |
| 224 | }); |
| 225 | |
| 226 | it('tells a member where an app reaches among their organizations', |
| 227 | const { body |
| 228 | .get(`/api/oauth-apps/${thirdParty |
| 229 | .set('cookie', |
| 230 | .expect(200); |
| 231 | expect(body.organizations |
| 232 | { |
| 233 | slug: org, |
| 234 | name: 'Access Org', |
| 235 | restricted: true, |
| 236 | allowed: false, |
| 237 | state: 'denied', |
| 238 | }, |
| 239 | ]); |
| 240 | |
| 241 | await restrict(false) |
| 242 | const open = |
| 243 | .get(`/api/oauth-apps/${thirdParty |
| 244 | .set('cookie', |
| 245 | .expect(200); |
| 246 | expect(open.body |
| 247 | restricted: false, |
| 248 | allowed: true, |
| 249 | }); |
| 250 | await rest(await |
| 251 | await api() |
| 252 | .get('/api/oauth-apps/nope/organizations') |
| 253 | .set('cookie', |
| 254 | .expect(404); |
| 255 | }); |
| 256 | }); |