Skip to content

Commit 9514542

Browse files
committed
Auth: Extend user accounts with custom scope setting
Signed-off-by: Michael Mayer <[email protected]>
1 parent c5312d6 commit 9514542

34 files changed

+562
-78
lines changed

frontend/src/common/config.js

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -674,9 +674,21 @@ export default class Config {
674674

675675
const albumsRoute = "albums";
676676
const browseRoute = "browse";
677-
const defaultRoute = this.deny("photos", "access_library") ? albumsRoute : browseRoute;
677+
const settingsRoute = "settings";
678678

679-
if (this.allow("settings", "update")) {
679+
let defaultRoute;
680+
681+
if (this.deny("photos", "access_library") || !this.feature("search")) {
682+
if (this.deny("albums", "view") || !this.feature("albums")) {
683+
defaultRoute = settingsRoute;
684+
} else {
685+
defaultRoute = albumsRoute;
686+
}
687+
} else {
688+
defaultRoute = browseRoute;
689+
}
690+
691+
if (defaultRoute !== settingsRoute && this.allow("settings", "update")) {
680692
const features = this.getSettings()?.features;
681693
const startPage = this.getSettings()?.ui?.startPage;
682694

@@ -704,6 +716,8 @@ export default class Config {
704716
return features.labels ? startPage : defaultRoute;
705717
case "folders":
706718
return features.folders ? startPage : defaultRoute;
719+
case "settings":
720+
return features.settings ? startPage : defaultRoute;
707721
default:
708722
return defaultRoute;
709723
}

frontend/src/common/session.js

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ export default class Session {
4646
this.config = config;
4747
this.provider = "";
4848
this.user = new User(false);
49+
this.scope = "";
4950
this.data = null;
5051

5152
// Set session storage.
@@ -106,6 +107,11 @@ export default class Session {
106107
if (provider !== null && provider !== "undefined") {
107108
this.provider = provider;
108109
}
110+
111+
const scope = this.storage.getItem(this.storageKey + ".scope");
112+
if (scope !== null && scope !== "undefined") {
113+
this.scope = scope;
114+
}
109115
}
110116

111117
// Authenticated?
@@ -219,11 +225,13 @@ export default class Session {
219225
this.id = null;
220226
this.authToken = null;
221227
this.provider = "";
228+
this.scope = "";
222229

223230
// "session.id" is the SHA256 hash of the auth token.
224231
this.storage.removeItem(this.storageKey + ".id");
225232
this.storage.removeItem(this.storageKey + ".token");
226233
this.storage.removeItem(this.storageKey + ".provider");
234+
this.storage.removeItem(this.storageKey + ".scope");
227235

228236
// Remove previously used data e.g. "session_id"
229237
// is deprecated in favor of "session.token".
@@ -292,6 +300,10 @@ export default class Session {
292300
this.setUser(resp.data.user);
293301
}
294302

303+
if (resp.data.scope) {
304+
this.setScope(resp.data.scope);
305+
}
306+
295307
if (resp.data.data) {
296308
this.setData(resp.data.data);
297309
}
@@ -340,6 +352,23 @@ export default class Session {
340352
return this.user;
341353
}
342354

355+
setScope(scope) {
356+
this.scope = scope;
357+
this.storage.setItem(this.storageKey + ".scope", scope);
358+
}
359+
360+
hasScope() {
361+
return Boolean(this.scope) && this.scope !== "*";
362+
}
363+
364+
getScope() {
365+
if (this.hasScope()) {
366+
return this.scope;
367+
}
368+
369+
return "*";
370+
}
371+
343372
getUserUID() {
344373
if (this.user && this.user.UID) {
345374
return this.user.UID;

frontend/src/component/navigation.vue

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -998,6 +998,7 @@ export default {
998998
const isReadOnly = this.$config.get("readonly");
999999
const isRestricted = this.$config.deny("photos", "access_library");
10001000
const isSuperAdmin = this.$session.isSuperAdmin();
1001+
const hasScope = this.$session.hasScope();
10011002
const tier = this.$config.getTier();
10021003
10031004
return {
@@ -1017,7 +1018,7 @@ export default {
10171018
drawer: null,
10181019
featUpgrade: tier < 6 && isSuperAdmin && !isPublic && !isDemo,
10191020
featMembership: tier < 3 && isSuperAdmin && !isPublic && !isDemo,
1020-
featFeedback: tier >= 6 && isSuperAdmin && !isPublic && !isDemo,
1021+
featFeedback: !hasScope && tier >= 6 && isSuperAdmin && !isPublic && !isDemo,
10211022
featFiles: this.$config.feature("files"),
10221023
featUsage: canManagePhotos && this.$config.feature("files") && this.$config.values?.usage?.filesTotal,
10231024
isRestricted: isRestricted,

frontend/src/model/user.js

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ export class User extends RestModel {
2828
Email: "",
2929
BackupEmail: "",
3030
Role: "",
31+
Scope: "",
3132
Attr: "",
3233
SuperAdmin: false,
3334
CanLogin: false,
@@ -200,6 +201,18 @@ export class User extends RestModel {
200201
.then((response) => Promise.resolve(new Form(response.data)));
201202
}
202203

204+
hasScope() {
205+
return Boolean(this.Scope) && this.Scope !== "*";
206+
}
207+
208+
getScope() {
209+
if (this.hasScope()) {
210+
return this.Scope;
211+
}
212+
213+
return "*";
214+
}
215+
203216
isRemote() {
204217
return this.AuthProvider && this.AuthProvider === "ldap";
205218
}

frontend/src/options/options.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -209,6 +209,7 @@ export const StartPages = (features, isPortal) => {
209209
{ value: "moments", text: $gettext("Moments"), props: { disabled: !features?.moments } },
210210
{ value: "labels", text: $gettext("Labels"), props: { disabled: !features?.labels } },
211211
{ value: "folders", text: $gettext("Folders"), props: { disabled: !features?.folders } },
212+
{ value: "settings", text: $gettext("Settings"), props: { disabled: !features?.settings } },
212213
];
213214
};
214215

frontend/src/page/settings/general.vue

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@
7979
</v-card-actions>
8080
</v-card>
8181

82-
<v-card v-if="!isPortal && (isDemo || isSuperAdmin)" flat tile class="mt-0 px-1 bg-background">
82+
<v-card v-if="!isPortal && !hasScope && (isDemo || isSuperAdmin)" flat tile class="mt-0 px-1 bg-background">
8383
<v-card-actions>
8484
<v-row align="start" dense>
8585
<v-col cols="12" sm="6" lg="3" class="px-2 pb-2 pt-2">
@@ -436,6 +436,7 @@ export default {
436436
return {
437437
isDemo: this.$config.isDemo(),
438438
isAdmin: this.$session.isAdmin(),
439+
hasScope: this.$session.hasScope(),
439440
isSuperAdmin: this.$session.isSuperAdmin(),
440441
isPublic: this.$config.isPublic(),
441442
isPortal: this.$config.isPortal(),

frontend/tests/vitest/common/config.test.js

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,11 @@ import * as themes from "options/themes";
66

77
const defaultConfig = new Config(new StorageShim(), window.__CONFIG__);
88

9+
const createTestConfig = () => {
10+
const values = JSON.parse(JSON.stringify(window.__CONFIG__));
11+
return new Config(new StorageShim(), values);
12+
};
13+
914
describe("common/config", () => {
1015
it("should get all config values", () => {
1116
const storage = new StorageShim();
@@ -164,6 +169,86 @@ describe("common/config", () => {
164169
expect(defaultConfig.feature("download")).toBe(true);
165170
});
166171

172+
it("returns albums when library access is restricted", () => {
173+
const cfg = createTestConfig();
174+
const settings = JSON.parse(JSON.stringify(cfg.getSettings()));
175+
settings.features = {
176+
...settings.features,
177+
search: true,
178+
albums: true,
179+
settings: true,
180+
};
181+
cfg.set("settings", settings);
182+
cfg.set("acl", {
183+
photos: { full_access: false, access_library: false },
184+
albums: { full_access: false, view: true },
185+
settings: { full_access: false, update: false },
186+
});
187+
188+
expect(cfg.getDefaultRoute()).toBe("albums");
189+
});
190+
191+
it("returns settings when library and albums are unavailable", () => {
192+
const cfg = createTestConfig();
193+
const settings = JSON.parse(JSON.stringify(cfg.getSettings()));
194+
settings.features = {
195+
...settings.features,
196+
search: true,
197+
albums: false,
198+
settings: true,
199+
};
200+
cfg.set("settings", settings);
201+
cfg.set("acl", {
202+
photos: { full_access: false, access_library: false },
203+
albums: { full_access: false, view: false },
204+
settings: { full_access: false, update: false },
205+
});
206+
207+
expect(cfg.getDefaultRoute()).toBe("settings");
208+
});
209+
210+
it("honors settings start page when permitted", () => {
211+
const cfg = createTestConfig();
212+
const settings = JSON.parse(JSON.stringify(cfg.getSettings()));
213+
settings.ui = {
214+
...settings.ui,
215+
startPage: "settings",
216+
};
217+
settings.features = {
218+
...settings.features,
219+
search: true,
220+
settings: true,
221+
};
222+
cfg.set("settings", settings);
223+
cfg.set("acl", {
224+
photos: { full_access: false, access_library: true },
225+
settings: { full_access: false, update: true },
226+
});
227+
228+
expect(cfg.getDefaultRoute()).toBe("settings");
229+
});
230+
231+
it("falls back to default route when settings feature is disabled", () => {
232+
const cfg = createTestConfig();
233+
const settings = JSON.parse(JSON.stringify(cfg.getSettings()));
234+
settings.ui = {
235+
...settings.ui,
236+
startPage: "settings",
237+
};
238+
settings.features = {
239+
...settings.features,
240+
search: true,
241+
settings: false,
242+
};
243+
cfg.set("settings", settings);
244+
cfg.set("acl", {
245+
photos: { full_access: false, access_library: true },
246+
settings: { full_access: false, update: true },
247+
});
248+
249+
expect(cfg.getDefaultRoute()).toBe("browse");
250+
});
251+
167252
it("should test get name", () => {
168253
const result = defaultConfig.getPerson("a");
169254
expect(result).toBeNull();

frontend/tests/vitest/common/session.test.js

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,28 @@ describe("common/session", () => {
186186
session.deleteData();
187187
});
188188

189+
it("should manage scope state", () => {
190+
const storage = new StorageShim();
191+
const session = new Session(storage, $config);
192+
193+
// Default scope is unrestricted.
194+
expect(session.hasScope()).toBe(false);
195+
expect(session.getScope()).toBe("*");
196+
197+
session.setId("a9b8ff820bf40ab451910f8bbfe401b2432446693aa539538fbd2399560a722f");
198+
session.setAuthToken("234200000000000000000000000000000000000000000000");
199+
session.setScope("photos:view");
200+
expect(session.hasScope()).toBe(true);
201+
expect(session.getScope()).toBe("photos:view");
202+
203+
// Scope flag should survive re-instantiation with the same storage.
204+
const restoredSession = new Session(storage, $config);
205+
expect(restoredSession.hasScope()).toBe(true);
206+
expect(restoredSession.getScope()).toBe("photos:view");
207+
208+
session.deleteAuthentication();
209+
});
210+
189211
it("should test whether user is set", () => {
190212
const storage = new StorageShim();
191213
const session = new Session(storage, $config);

frontend/tests/vitest/fixtures.js

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,21 @@ Mock.onPost("api/v1/session").reply(
133133
access_token: "999900000000000000000000000000000000000000000000",
134134
token_type: "Bearer",
135135
provider: "test",
136+
scope: "photos:view",
137+
data: { token: "123token" },
138+
user: { ID: 1, UID: "urjysof3b9v7lgex", Name: "test", Email: "[email protected]" },
139+
},
140+
mockHeaders
141+
);
142+
143+
Mock.onGet("api/v1/session").reply(
144+
200,
145+
{
146+
session_id: "5aa770f2a1ef431628d9f17bdf82a0d16865e99d4a1ddd9356e1aabfe6464683",
147+
access_token: "999900000000000000000000000000000000000000000000",
148+
token_type: "Bearer",
149+
provider: "test",
150+
scope: "photos:view",
136151
data: { token: "123token" },
137152
user: { ID: 1, UID: "urjysof3b9v7lgex", Name: "test", Email: "[email protected]" },
138153
},

frontend/tests/vitest/model/user.test.js

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -197,6 +197,16 @@ describe("model/user", () => {
197197
expect(result).toBe("Max Last");
198198
});
199199

200+
it("should manage scope helpers", () => {
201+
const unrestricted = new User({ Scope: "*" });
202+
expect(unrestricted.hasScope()).toBe(false);
203+
expect(unrestricted.getScope()).toBe("*");
204+
205+
const restricted = new User({ Scope: "photos:view" });
206+
expect(restricted.hasScope()).toBe(true);
207+
expect(restricted.getScope()).toBe("photos:view");
208+
});
209+
200210
it("should get id", () => {
201211
const values = {
202212
ID: 5,

0 commit comments

Comments
 (0)