Browse Source

feat(server): storage label claim (#3278)

* feat: storage label claim

* chore: open api
Jason Rasmussen 2 years ago
parent
commit
f55d63fae8

+ 6 - 0
cli/src/api/open-api/api.ts

@@ -2596,6 +2596,12 @@ export interface SystemConfigOAuthDto {
      * @memberof SystemConfigOAuthDto
      * @memberof SystemConfigOAuthDto
      */
      */
     'scope': string;
     'scope': string;
+    /**
+     * 
+     * @type {string}
+     * @memberof SystemConfigOAuthDto
+     */
+    'storageLabelClaim': string;
     /**
     /**
      * 
      * 
      * @type {string}
      * @type {string}

+ 1 - 0
mobile/openapi/doc/SystemConfigOAuthDto.md

@@ -13,6 +13,7 @@ Name | Type | Description | Notes
 **clientId** | **String** |  | 
 **clientId** | **String** |  | 
 **clientSecret** | **String** |  | 
 **clientSecret** | **String** |  | 
 **scope** | **String** |  | 
 **scope** | **String** |  | 
+**storageLabelClaim** | **String** |  | 
 **buttonText** | **String** |  | 
 **buttonText** | **String** |  | 
 **autoRegister** | **bool** |  | 
 **autoRegister** | **bool** |  | 
 **autoLaunch** | **bool** |  | 
 **autoLaunch** | **bool** |  | 

+ 9 - 1
mobile/openapi/lib/model/system_config_o_auth_dto.dart

@@ -18,6 +18,7 @@ class SystemConfigOAuthDto {
     required this.clientId,
     required this.clientId,
     required this.clientSecret,
     required this.clientSecret,
     required this.scope,
     required this.scope,
+    required this.storageLabelClaim,
     required this.buttonText,
     required this.buttonText,
     required this.autoRegister,
     required this.autoRegister,
     required this.autoLaunch,
     required this.autoLaunch,
@@ -35,6 +36,8 @@ class SystemConfigOAuthDto {
 
 
   String scope;
   String scope;
 
 
+  String storageLabelClaim;
+
   String buttonText;
   String buttonText;
 
 
   bool autoRegister;
   bool autoRegister;
@@ -52,6 +55,7 @@ class SystemConfigOAuthDto {
      other.clientId == clientId &&
      other.clientId == clientId &&
      other.clientSecret == clientSecret &&
      other.clientSecret == clientSecret &&
      other.scope == scope &&
      other.scope == scope &&
+     other.storageLabelClaim == storageLabelClaim &&
      other.buttonText == buttonText &&
      other.buttonText == buttonText &&
      other.autoRegister == autoRegister &&
      other.autoRegister == autoRegister &&
      other.autoLaunch == autoLaunch &&
      other.autoLaunch == autoLaunch &&
@@ -66,6 +70,7 @@ class SystemConfigOAuthDto {
     (clientId.hashCode) +
     (clientId.hashCode) +
     (clientSecret.hashCode) +
     (clientSecret.hashCode) +
     (scope.hashCode) +
     (scope.hashCode) +
+    (storageLabelClaim.hashCode) +
     (buttonText.hashCode) +
     (buttonText.hashCode) +
     (autoRegister.hashCode) +
     (autoRegister.hashCode) +
     (autoLaunch.hashCode) +
     (autoLaunch.hashCode) +
@@ -73,7 +78,7 @@ class SystemConfigOAuthDto {
     (mobileRedirectUri.hashCode);
     (mobileRedirectUri.hashCode);
 
 
   @override
   @override
-  String toString() => 'SystemConfigOAuthDto[enabled=$enabled, issuerUrl=$issuerUrl, clientId=$clientId, clientSecret=$clientSecret, scope=$scope, buttonText=$buttonText, autoRegister=$autoRegister, autoLaunch=$autoLaunch, mobileOverrideEnabled=$mobileOverrideEnabled, mobileRedirectUri=$mobileRedirectUri]';
+  String toString() => 'SystemConfigOAuthDto[enabled=$enabled, issuerUrl=$issuerUrl, clientId=$clientId, clientSecret=$clientSecret, scope=$scope, storageLabelClaim=$storageLabelClaim, buttonText=$buttonText, autoRegister=$autoRegister, autoLaunch=$autoLaunch, mobileOverrideEnabled=$mobileOverrideEnabled, mobileRedirectUri=$mobileRedirectUri]';
 
 
   Map<String, dynamic> toJson() {
   Map<String, dynamic> toJson() {
     final json = <String, dynamic>{};
     final json = <String, dynamic>{};
@@ -82,6 +87,7 @@ class SystemConfigOAuthDto {
       json[r'clientId'] = this.clientId;
       json[r'clientId'] = this.clientId;
       json[r'clientSecret'] = this.clientSecret;
       json[r'clientSecret'] = this.clientSecret;
       json[r'scope'] = this.scope;
       json[r'scope'] = this.scope;
+      json[r'storageLabelClaim'] = this.storageLabelClaim;
       json[r'buttonText'] = this.buttonText;
       json[r'buttonText'] = this.buttonText;
       json[r'autoRegister'] = this.autoRegister;
       json[r'autoRegister'] = this.autoRegister;
       json[r'autoLaunch'] = this.autoLaunch;
       json[r'autoLaunch'] = this.autoLaunch;
@@ -103,6 +109,7 @@ class SystemConfigOAuthDto {
         clientId: mapValueOfType<String>(json, r'clientId')!,
         clientId: mapValueOfType<String>(json, r'clientId')!,
         clientSecret: mapValueOfType<String>(json, r'clientSecret')!,
         clientSecret: mapValueOfType<String>(json, r'clientSecret')!,
         scope: mapValueOfType<String>(json, r'scope')!,
         scope: mapValueOfType<String>(json, r'scope')!,
+        storageLabelClaim: mapValueOfType<String>(json, r'storageLabelClaim')!,
         buttonText: mapValueOfType<String>(json, r'buttonText')!,
         buttonText: mapValueOfType<String>(json, r'buttonText')!,
         autoRegister: mapValueOfType<bool>(json, r'autoRegister')!,
         autoRegister: mapValueOfType<bool>(json, r'autoRegister')!,
         autoLaunch: mapValueOfType<bool>(json, r'autoLaunch')!,
         autoLaunch: mapValueOfType<bool>(json, r'autoLaunch')!,
@@ -160,6 +167,7 @@ class SystemConfigOAuthDto {
     'clientId',
     'clientId',
     'clientSecret',
     'clientSecret',
     'scope',
     'scope',
+    'storageLabelClaim',
     'buttonText',
     'buttonText',
     'autoRegister',
     'autoRegister',
     'autoLaunch',
     'autoLaunch',

+ 5 - 0
mobile/openapi/test/system_config_o_auth_dto_test.dart

@@ -41,6 +41,11 @@ void main() {
       // TODO
       // TODO
     });
     });
 
 
+    // String storageLabelClaim
+    test('to test the property `storageLabelClaim`', () async {
+      // TODO
+    });
+
     // String buttonText
     // String buttonText
     test('to test the property `buttonText`', () async {
     test('to test the property `buttonText`', () async {
       // TODO
       // TODO

+ 4 - 0
server/immich-openapi-specs.json

@@ -6503,6 +6503,9 @@
           "scope": {
           "scope": {
             "type": "string"
             "type": "string"
           },
           },
+          "storageLabelClaim": {
+            "type": "string"
+          },
           "buttonText": {
           "buttonText": {
             "type": "string"
             "type": "string"
           },
           },
@@ -6525,6 +6528,7 @@
           "clientId",
           "clientId",
           "clientSecret",
           "clientSecret",
           "scope",
           "scope",
+          "storageLabelClaim",
           "buttonText",
           "buttonText",
           "autoRegister",
           "autoRegister",
           "autoLaunch",
           "autoLaunch",

+ 8 - 0
server/src/domain/auth/auth.service.ts

@@ -240,11 +240,19 @@ export class AuthService {
       }
       }
 
 
       this.logger.log(`Registering new user: ${profile.email}/${profile.sub}`);
       this.logger.log(`Registering new user: ${profile.email}/${profile.sub}`);
+      this.logger.verbose(`OAuth Profile: ${JSON.stringify(profile)}`);
+
+      let storageLabel: string | null = profile[config.oauth.storageLabelClaim as keyof OAuthProfile] as string;
+      if (typeof storageLabel !== 'string') {
+        storageLabel = null;
+      }
+
       user = await this.userCore.createUser({
       user = await this.userCore.createUser({
         firstName: profile.given_name || '',
         firstName: profile.given_name || '',
         lastName: profile.family_name || '',
         lastName: profile.family_name || '',
         email: profile.email,
         email: profile.email,
         oauthId: profile.sub,
         oauthId: profile.sub,
+        storageLabel,
       });
       });
     }
     }
 
 

+ 3 - 0
server/src/domain/system-config/dto/system-config-oauth.dto.ts

@@ -25,6 +25,9 @@ export class SystemConfigOAuthDto {
   @IsString()
   @IsString()
   scope!: string;
   scope!: string;
 
 
+  @IsString()
+  storageLabelClaim!: string;
+
   @IsString()
   @IsString()
   buttonText!: string;
   buttonText!: string;
 
 

+ 1 - 0
server/src/domain/system-config/system-config.core.ts

@@ -48,6 +48,7 @@ export const defaults = Object.freeze<SystemConfig>({
     mobileOverrideEnabled: false,
     mobileOverrideEnabled: false,
     mobileRedirectUri: '',
     mobileRedirectUri: '',
     scope: 'openid email profile',
     scope: 'openid email profile',
+    storageLabelClaim: 'preferred_username',
     buttonText: 'Login with OAuth',
     buttonText: 'Login with OAuth',
     autoRegister: true,
     autoRegister: true,
     autoLaunch: false,
     autoLaunch: false,

+ 1 - 0
server/src/domain/system-config/system-config.service.spec.ts

@@ -53,6 +53,7 @@ const updatedConfig = Object.freeze<SystemConfig>({
     mobileOverrideEnabled: false,
     mobileOverrideEnabled: false,
     mobileRedirectUri: '',
     mobileRedirectUri: '',
     scope: 'openid email profile',
     scope: 'openid email profile',
+    storageLabelClaim: 'preferred_username',
   },
   },
   passwordLogin: {
   passwordLogin: {
     enabled: true,
     enabled: true,

+ 8 - 5
server/src/domain/user/user.core.ts

@@ -8,9 +8,9 @@ import {
 } from '@nestjs/common';
 } from '@nestjs/common';
 import { constants, createReadStream, ReadStream } from 'fs';
 import { constants, createReadStream, ReadStream } from 'fs';
 import fs from 'fs/promises';
 import fs from 'fs/promises';
+import sanitize from 'sanitize-filename';
 import { AuthUserDto } from '../auth';
 import { AuthUserDto } from '../auth';
 import { ICryptoRepository } from '../crypto';
 import { ICryptoRepository } from '../crypto';
-import { CreateAdminDto, CreateUserDto, CreateUserOAuthDto } from './dto/create-user.dto';
 import { IUserRepository, UserListFilter } from './user.repository';
 import { IUserRepository, UserListFilter } from './user.repository';
 
 
 const SALT_ROUNDS = 10;
 const SALT_ROUNDS = 10;
@@ -67,13 +67,13 @@ export class UserCore {
     }
     }
   }
   }
 
 
-  async createUser(createUserDto: CreateUserDto | CreateAdminDto | CreateUserOAuthDto): Promise<UserEntity> {
-    const user = await this.userRepository.getByEmail(createUserDto.email);
+  async createUser(dto: Partial<UserEntity> & { email: string }): Promise<UserEntity> {
+    const user = await this.userRepository.getByEmail(dto.email);
     if (user) {
     if (user) {
       throw new BadRequestException('User exists');
       throw new BadRequestException('User exists');
     }
     }
 
 
-    if (!(createUserDto as CreateAdminDto).isAdmin) {
+    if (!dto.isAdmin) {
       const localAdmin = await this.userRepository.getAdmin();
       const localAdmin = await this.userRepository.getAdmin();
       if (!localAdmin) {
       if (!localAdmin) {
         throw new BadRequestException('The first registered account must the administrator.');
         throw new BadRequestException('The first registered account must the administrator.');
@@ -81,10 +81,13 @@ export class UserCore {
     }
     }
 
 
     try {
     try {
-      const payload: Partial<UserEntity> = { ...createUserDto };
+      const payload: Partial<UserEntity> = { ...dto };
       if (payload.password) {
       if (payload.password) {
         payload.password = await this.cryptoRepository.hashBcrypt(payload.password, SALT_ROUNDS);
         payload.password = await this.cryptoRepository.hashBcrypt(payload.password, SALT_ROUNDS);
       }
       }
+      if (payload.storageLabel) {
+        payload.storageLabel = sanitize(payload.storageLabel);
+      }
       return this.userRepository.create(payload);
       return this.userRepository.create(payload);
     } catch (e) {
     } catch (e) {
       Logger.error(e, 'Create new user');
       Logger.error(e, 'Create new user');

+ 2 - 0
server/src/infra/entities/system-config.entity.ts

@@ -40,6 +40,7 @@ export enum SystemConfigKey {
   OAUTH_CLIENT_ID = 'oauth.clientId',
   OAUTH_CLIENT_ID = 'oauth.clientId',
   OAUTH_CLIENT_SECRET = 'oauth.clientSecret',
   OAUTH_CLIENT_SECRET = 'oauth.clientSecret',
   OAUTH_SCOPE = 'oauth.scope',
   OAUTH_SCOPE = 'oauth.scope',
+  OAUTH_STORAGE_LABEL_CLAIM = 'oauth.storageLabelClaim',
   OAUTH_AUTO_LAUNCH = 'oauth.autoLaunch',
   OAUTH_AUTO_LAUNCH = 'oauth.autoLaunch',
   OAUTH_BUTTON_TEXT = 'oauth.buttonText',
   OAUTH_BUTTON_TEXT = 'oauth.buttonText',
   OAUTH_AUTO_REGISTER = 'oauth.autoRegister',
   OAUTH_AUTO_REGISTER = 'oauth.autoRegister',
@@ -89,6 +90,7 @@ export interface SystemConfig {
     clientId: string;
     clientId: string;
     clientSecret: string;
     clientSecret: string;
     scope: string;
     scope: string;
+    storageLabelClaim: string;
     buttonText: string;
     buttonText: string;
     autoRegister: boolean;
     autoRegister: boolean;
     autoLaunch: boolean;
     autoLaunch: boolean;

+ 6 - 0
web/src/api/open-api/api.ts

@@ -2596,6 +2596,12 @@ export interface SystemConfigOAuthDto {
      * @memberof SystemConfigOAuthDto
      * @memberof SystemConfigOAuthDto
      */
      */
     'scope': string;
     'scope': string;
+    /**
+     * 
+     * @type {string}
+     * @memberof SystemConfigOAuthDto
+     */
+    'storageLabelClaim': string;
     /**
     /**
      * 
      * 
      * @type {string}
      * @type {string}

+ 10 - 0
web/src/lib/components/admin-page/settings/oauth/oauth-settings.svelte

@@ -155,6 +155,16 @@
           isEdited={!(oauthConfig.scope == savedConfig.scope)}
           isEdited={!(oauthConfig.scope == savedConfig.scope)}
         />
         />
 
 
+        <SettingInputField
+          inputType={SettingInputFieldType.TEXT}
+          label="STORAGE LABEL CLAIM"
+          desc="Automatically set the user's storage label to the value of this claim."
+          bind:value={oauthConfig.storageLabelClaim}
+          required={true}
+          disabled={!oauthConfig.storageLabelClaim}
+          isEdited={!(oauthConfig.storageLabelClaim == savedConfig.storageLabelClaim)}
+        />
+
         <SettingInputField
         <SettingInputField
           inputType={SettingInputFieldType.TEXT}
           inputType={SettingInputFieldType.TEXT}
           label="BUTTON TEXT"
           label="BUTTON TEXT"