mirror of
				https://github.com/mattermost/mattermost.git
				synced 2025-02-25 18:55:24 -06:00 
			
		
		
		
	Custom profile attributes field endpoints (#29662)
* Adds the main Property System Architecture components
This change adds the necessary migrations for the Property Groups,
Fields and Values tables to be created, the store layer and a Property
Service that can be used from the app layer.
* Adds Custom Profile Attributes endpoints and app layer
* implement get and patch cpa values
* run i18n-extract
* Update property field type to use user instead of person
* Update PropertyFields to allow for unique nondeleted fields and remove redundant indexes
* Update PropertyValues to allow for unique nondeleted fields and remove redundant indexes
* Use StringMap instead of the map[string]any on property fields
* Add i18n strings
* Revert "Use StringMap instead of the map[string]any on property fields"
This reverts commit e2735ab0f8.
* Cast JSON binary data to string and add todo note for StringMap use
* Add mocks to the retrylayer tests
* Cast JSON binary data to string in property value store
* Check for binary parameter instead of casting to string for JSON data
* Fix bad merge
* Check property field type is one of the allowed ones
* Avoid reusing err variable to be explicit about the returned value
* Merge Property System Migrations into one file
* Adds NOT NULL to timestamps at the DB level
* Update stores to use tableSelectQuery instead of a slice var
* Update PropertyField model translations to be more explicit and avoid repetition
* Update PropertyValue model translations to be more explicit and avoid repetition
* Use ExecBuilder instead of ToSql&Exec
* Update property field errors to add context
* Ensure PerPage is greater than zero
* Update store errors to give more context
* Use ExecBuilder in the property stores where possible
* Add an on conflict suffix to the group register to avoid race conditions
* Remove user profile API documentation changes
* Update patchCPAValues endpoint and docs to return the updated information
* Merge two similar error conditions
* Use a route function for ListCPAValues
* Remove badly used translation string
* Remove unused get in register group method
* Adds input sanitization and validation to the CPA API endpoints
* Takes login outside of one test case to make it clear it affects multiple t.Runs
* Fix wrap error and return code when property field has been deleted
* Fix receiver name
* Adds comment to move the CPA group ID to the db cache
* Set the PerPage of CPA fields to the fields limit
* Update server/channels/app/custom_profile_attributes_test.go
Co-authored-by: Alejandro García Montoro <alejandro.garciamontoro@gmail.com>
* Standardize group ID access
* Avoid polluting the state between tests
* Use specific errors for the retrieval of CPA group
---------
Co-authored-by: Scott Bishel <scott.bishel@mattermost.com>
Co-authored-by: Mattermost Build <build@mattermost.com>
Co-authored-by: Alejandro García Montoro <alejandro.garciamontoro@gmail.com>
			
			
This commit is contained in:
		
				
					committed by
					
						
						GitHub
					
				
			
			
				
	
			
			
			
						parent
						
							c6984941f1
						
					
				
				
					commit
					ca34c6a03f
				
			@@ -58,6 +58,7 @@ build-v4: node_modules playbooks
 | 
			
		||||
	@cat $(V4_SRC)/outgoing_oauth_connections.yaml >> $(V4_YAML)
 | 
			
		||||
	@cat $(V4_SRC)/metrics.yaml >> $(V4_YAML)
 | 
			
		||||
	@cat $(V4_SRC)/scheduled_post.yaml >> $(V4_YAML)
 | 
			
		||||
	@cat $(V4_SRC)/custom_profile_attributes.yaml >> $(V4_YAML)
 | 
			
		||||
	@if [ -r $(PLAYBOOKS_SRC)/paths.yaml ]; then cat $(PLAYBOOKS_SRC)/paths.yaml >> $(V4_YAML); fi
 | 
			
		||||
	@if [ -r $(PLAYBOOKS_SRC)/merged-definitions.yaml ]; then cat $(PLAYBOOKS_SRC)/merged-definitions.yaml >> $(V4_YAML); else cat $(V4_SRC)/definitions.yaml >> $(V4_YAML); fi
 | 
			
		||||
	@echo Extracting code samples
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										257
									
								
								api/v4/source/custom_profile_attributes.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										257
									
								
								api/v4/source/custom_profile_attributes.yaml
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,257 @@
 | 
			
		||||
  "/api/v4/custom_profile_attributes/fields":
 | 
			
		||||
    get:
 | 
			
		||||
      tags:
 | 
			
		||||
        - custom profile attributes
 | 
			
		||||
      summary: List all the Custom Profile Attributes fields
 | 
			
		||||
      description: |
 | 
			
		||||
        List all the Custom Profile Attributes fields.
 | 
			
		||||
 | 
			
		||||
        _This endpoint is experimental._
 | 
			
		||||
 | 
			
		||||
        __Minimum server version__: 10.5
 | 
			
		||||
 | 
			
		||||
        ##### Permissions
 | 
			
		||||
        Must be authenticated.
 | 
			
		||||
      operationId: ListAllCPAFields
 | 
			
		||||
      responses:
 | 
			
		||||
        "200":
 | 
			
		||||
          description: Custom Profile Attributes fetch successful. Result may be empty.
 | 
			
		||||
          content:
 | 
			
		||||
            application/json:
 | 
			
		||||
              schema:
 | 
			
		||||
                type: array
 | 
			
		||||
                items:
 | 
			
		||||
                  $ref: "#/components/schemas/PropertyField"
 | 
			
		||||
        "401":
 | 
			
		||||
          $ref: "#/components/responses/Unauthorized"
 | 
			
		||||
 | 
			
		||||
    post:
 | 
			
		||||
      tags:
 | 
			
		||||
        - custom profile attributes
 | 
			
		||||
      summary: Create a Custom Profile Attribute field
 | 
			
		||||
      description: |
 | 
			
		||||
        Create a new Custom Profile Attribute field on the system.
 | 
			
		||||
 | 
			
		||||
        _This endpoint is experimental._
 | 
			
		||||
 | 
			
		||||
        __Minimum server version__: 10.5
 | 
			
		||||
 | 
			
		||||
        ##### Permissions
 | 
			
		||||
        Must have `manage_system` permission.
 | 
			
		||||
      operationId: CreateCPAField
 | 
			
		||||
      requestBody:
 | 
			
		||||
        content:
 | 
			
		||||
          application/json:
 | 
			
		||||
            schema:
 | 
			
		||||
              type: object
 | 
			
		||||
              required:
 | 
			
		||||
                - name
 | 
			
		||||
                - type
 | 
			
		||||
              properties:
 | 
			
		||||
                name:
 | 
			
		||||
                  type: string
 | 
			
		||||
                type:
 | 
			
		||||
                  type: string
 | 
			
		||||
                attrs:
 | 
			
		||||
                  type: string
 | 
			
		||||
      responses:
 | 
			
		||||
        "201":
 | 
			
		||||
          description: Custom Profile Attribute field creation successful
 | 
			
		||||
          content:
 | 
			
		||||
            application/json:
 | 
			
		||||
              schema:
 | 
			
		||||
                $ref: "#/components/schemas/PropertyField"
 | 
			
		||||
        "400":
 | 
			
		||||
          $ref: "#/components/responses/BadRequest"
 | 
			
		||||
        "401":
 | 
			
		||||
          $ref: "#/components/responses/Unauthorized"
 | 
			
		||||
        "403":
 | 
			
		||||
          $ref: "#/components/responses/Forbidden"
 | 
			
		||||
 | 
			
		||||
  "/api/v4/custom_profile_attributes/fields/{field_id}":
 | 
			
		||||
    patch:
 | 
			
		||||
      tags:
 | 
			
		||||
        - custom profile attributes
 | 
			
		||||
      summary: Patch a Custom Profile Attribute field
 | 
			
		||||
      description: |
 | 
			
		||||
        Partially update a Custom Profile Attribute field by providing
 | 
			
		||||
        only the fields you want to update. Omitted fields will not be
 | 
			
		||||
        updated. The fields that can be updated are defined in the
 | 
			
		||||
        request body, all other provided fields will be ignored.
 | 
			
		||||
 | 
			
		||||
        _This endpoint is experimental._
 | 
			
		||||
 | 
			
		||||
        __Minimum server version__: 10.5
 | 
			
		||||
 | 
			
		||||
        ##### Permissions
 | 
			
		||||
        Must have `manage_system` permission.
 | 
			
		||||
      operationId: PatchCPAField
 | 
			
		||||
      parameters:
 | 
			
		||||
        - name: field_id
 | 
			
		||||
          in: path
 | 
			
		||||
          description: Custom Profile Attribute field GUID
 | 
			
		||||
          required: true
 | 
			
		||||
          schema:
 | 
			
		||||
            type: string
 | 
			
		||||
      requestBody:
 | 
			
		||||
        description: Custom Profile Attribute field that is to be updated
 | 
			
		||||
        required: true
 | 
			
		||||
        content:
 | 
			
		||||
          application/json:
 | 
			
		||||
            schema:
 | 
			
		||||
              type: object
 | 
			
		||||
              properties:
 | 
			
		||||
                name:
 | 
			
		||||
                  type: string
 | 
			
		||||
                type:
 | 
			
		||||
                  type: string
 | 
			
		||||
                attrs:
 | 
			
		||||
                  type: string
 | 
			
		||||
      responses:
 | 
			
		||||
        "200":
 | 
			
		||||
          description: Custom Profile Attribute field patch successful
 | 
			
		||||
          content:
 | 
			
		||||
            application/json:
 | 
			
		||||
              schema:
 | 
			
		||||
                $ref: "#/components/schemas/PropertyField"
 | 
			
		||||
        "400":
 | 
			
		||||
          $ref: "#/components/responses/BadRequest"
 | 
			
		||||
        "401":
 | 
			
		||||
          $ref: "#/components/responses/Unauthorized"
 | 
			
		||||
        "403":
 | 
			
		||||
          $ref: "#/components/responses/Forbidden"
 | 
			
		||||
 | 
			
		||||
    delete:
 | 
			
		||||
      tags:
 | 
			
		||||
        - custom profile attributes
 | 
			
		||||
      summary: Delete a Custom Profile Attribute field
 | 
			
		||||
      description: |
 | 
			
		||||
        Marks a Custom Profile Attribute field and all its values as
 | 
			
		||||
        deleted.
 | 
			
		||||
 | 
			
		||||
        _This endpoint is experimental._
 | 
			
		||||
 | 
			
		||||
        __Minimum server version__: 10.5
 | 
			
		||||
 | 
			
		||||
        ##### Permissions
 | 
			
		||||
        Must have `manage_system` permission.
 | 
			
		||||
      operationId: DeleteCPAField
 | 
			
		||||
      parameters:
 | 
			
		||||
        - name: field_id
 | 
			
		||||
          in: path
 | 
			
		||||
          description: Custom Profile Attribute field GUID
 | 
			
		||||
          required: true
 | 
			
		||||
          schema:
 | 
			
		||||
            type: string
 | 
			
		||||
      responses:
 | 
			
		||||
        "200":
 | 
			
		||||
          description: Custom Profile Attribute field deletion successful
 | 
			
		||||
          content:
 | 
			
		||||
            application/json:
 | 
			
		||||
              schema:
 | 
			
		||||
                $ref: "#/components/schemas/StatusOK"
 | 
			
		||||
        "400":
 | 
			
		||||
          $ref: "#/components/responses/BadRequest"
 | 
			
		||||
        "401":
 | 
			
		||||
          $ref: "#/components/responses/Unauthorized"
 | 
			
		||||
        "403":
 | 
			
		||||
          $ref: "#/components/responses/Forbidden"
 | 
			
		||||
 | 
			
		||||
  "/api/v4/custom_profile_attributes/values":
 | 
			
		||||
    patch:
 | 
			
		||||
      tags:
 | 
			
		||||
        - custom profile attributes
 | 
			
		||||
      summary: Patch Custom Profile Attribute values
 | 
			
		||||
      description: |
 | 
			
		||||
        Partially update a set of values on the requester's Custom
 | 
			
		||||
        Profile Attribute fields by providing only the information you
 | 
			
		||||
        want to update. Omitted fields will not be updated. The fields
 | 
			
		||||
        that can be updated are defined in the request body, all other
 | 
			
		||||
        provided fields will be ignored.
 | 
			
		||||
 | 
			
		||||
        _This endpoint is experimental._
 | 
			
		||||
 | 
			
		||||
        __Minimum server version__: 10.5
 | 
			
		||||
 | 
			
		||||
        ##### Permissions
 | 
			
		||||
        Must be authenticated.
 | 
			
		||||
      operationId: PatchCPAValues
 | 
			
		||||
      requestBody:
 | 
			
		||||
        description: Custom Profile Attribute values that are to be updated
 | 
			
		||||
        required: true
 | 
			
		||||
        content:
 | 
			
		||||
          application/json:
 | 
			
		||||
            schema:
 | 
			
		||||
              type: array
 | 
			
		||||
              items:
 | 
			
		||||
                type: object
 | 
			
		||||
                properties:
 | 
			
		||||
                  id:
 | 
			
		||||
                    type: string
 | 
			
		||||
                  value:
 | 
			
		||||
                    type: string
 | 
			
		||||
      responses:
 | 
			
		||||
        "200":
 | 
			
		||||
          description: Custom Profile Attribute values patch successful
 | 
			
		||||
          content:
 | 
			
		||||
            application/json:
 | 
			
		||||
              schema:
 | 
			
		||||
                type: array
 | 
			
		||||
                items:
 | 
			
		||||
                  type: object
 | 
			
		||||
                  properties:
 | 
			
		||||
                    id:
 | 
			
		||||
                      type: string
 | 
			
		||||
                    value:
 | 
			
		||||
                      type: string
 | 
			
		||||
        "400":
 | 
			
		||||
          $ref: "#/components/responses/BadRequest"
 | 
			
		||||
        "401":
 | 
			
		||||
          $ref: "#/components/responses/Unauthorized"
 | 
			
		||||
        "403":
 | 
			
		||||
          $ref: "#/components/responses/Forbidden"
 | 
			
		||||
  "/api/v4/users/{user_id}/custom_profile_attributes":
 | 
			
		||||
    get:
 | 
			
		||||
      tags:
 | 
			
		||||
        - custom profile attributes
 | 
			
		||||
      summary: List Custom Profile Attribute values
 | 
			
		||||
      description: |
 | 
			
		||||
        List all the Custom Profile Attributes values for specified user.
 | 
			
		||||
 | 
			
		||||
        _This endpoint is experimental._
 | 
			
		||||
 | 
			
		||||
        __Minimum server version__: 10.5
 | 
			
		||||
 | 
			
		||||
        ##### Permissions
 | 
			
		||||
        Must have `view members` permission.
 | 
			
		||||
      operationId: ListCPAValues
 | 
			
		||||
      parameters:
 | 
			
		||||
        - name: user_id
 | 
			
		||||
          in: path
 | 
			
		||||
          description: User GUID
 | 
			
		||||
          required: true
 | 
			
		||||
          schema:
 | 
			
		||||
            type: string
 | 
			
		||||
      responses:
 | 
			
		||||
        "200":
 | 
			
		||||
          description: Custom Profile Attribute values fetch successful. Result may be empty.
 | 
			
		||||
          content:
 | 
			
		||||
            application/json:
 | 
			
		||||
            schema:
 | 
			
		||||
              type: array
 | 
			
		||||
              items:
 | 
			
		||||
                type: object
 | 
			
		||||
                properties:
 | 
			
		||||
                  field_id:
 | 
			
		||||
                    type: string
 | 
			
		||||
                  value:
 | 
			
		||||
                    type: string
 | 
			
		||||
        "400":
 | 
			
		||||
          $ref: "#/components/responses/BadRequest"
 | 
			
		||||
        "401":
 | 
			
		||||
          $ref: "#/components/responses/Unauthorized"
 | 
			
		||||
        "403":
 | 
			
		||||
          $ref: "#/components/responses/Forbidden"
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@@ -404,6 +404,69 @@ components:
 | 
			
		||||
          type: string
 | 
			
		||||
        metadata:
 | 
			
		||||
          $ref: "#/components/schemas/PostMetadata"
 | 
			
		||||
    PropertyField:
 | 
			
		||||
      type: object
 | 
			
		||||
      properties:
 | 
			
		||||
        id:
 | 
			
		||||
          type: string
 | 
			
		||||
        group_id:
 | 
			
		||||
          type: string
 | 
			
		||||
        name:
 | 
			
		||||
          type: string
 | 
			
		||||
        type:
 | 
			
		||||
          type: string
 | 
			
		||||
        attrs:
 | 
			
		||||
          type: object
 | 
			
		||||
        target_id:
 | 
			
		||||
          type: string
 | 
			
		||||
        target_type:
 | 
			
		||||
          type: string
 | 
			
		||||
        create_at:
 | 
			
		||||
          type: integer
 | 
			
		||||
          format: int64
 | 
			
		||||
        update_at:
 | 
			
		||||
          type: integer
 | 
			
		||||
          format: int64
 | 
			
		||||
        delete_at:
 | 
			
		||||
          type: integer
 | 
			
		||||
          format: int64
 | 
			
		||||
    PropertyFieldPatch:
 | 
			
		||||
      type: object
 | 
			
		||||
      properties:
 | 
			
		||||
        name:
 | 
			
		||||
          type: string
 | 
			
		||||
        type:
 | 
			
		||||
          type: string
 | 
			
		||||
        attrs:
 | 
			
		||||
          type: object
 | 
			
		||||
        target_id:
 | 
			
		||||
          type: string
 | 
			
		||||
        target_type:
 | 
			
		||||
          type: string
 | 
			
		||||
    PropertyValue:
 | 
			
		||||
      type: object
 | 
			
		||||
      properties:
 | 
			
		||||
        id:
 | 
			
		||||
          type: string
 | 
			
		||||
        target_id:
 | 
			
		||||
          type: string
 | 
			
		||||
        target_type:
 | 
			
		||||
          type: string
 | 
			
		||||
        group_id:
 | 
			
		||||
          type: string
 | 
			
		||||
        field_id:
 | 
			
		||||
          type: string
 | 
			
		||||
        value:
 | 
			
		||||
          type: string
 | 
			
		||||
        create_at:
 | 
			
		||||
          type: integer
 | 
			
		||||
          format: int64
 | 
			
		||||
        update_at:
 | 
			
		||||
          type: integer
 | 
			
		||||
          format: int64
 | 
			
		||||
        delete_at:
 | 
			
		||||
          type: integer
 | 
			
		||||
          format: int64
 | 
			
		||||
    FileInfoList:
 | 
			
		||||
      type: object
 | 
			
		||||
      properties:
 | 
			
		||||
 
 | 
			
		||||
@@ -614,6 +614,7 @@ x-tagGroups:
 | 
			
		||||
      - exports
 | 
			
		||||
      - usage
 | 
			
		||||
      - reports
 | 
			
		||||
      - custom profile attributes
 | 
			
		||||
servers:
 | 
			
		||||
  - url: http://your-mattermost-url.com
 | 
			
		||||
  - url: https://your-mattermost-url.com
 | 
			
		||||
 
 | 
			
		||||
@@ -756,6 +756,12 @@
 | 
			
		||||
          required: true
 | 
			
		||||
          schema:
 | 
			
		||||
            type: string
 | 
			
		||||
        - name: cpa
 | 
			
		||||
          in: query
 | 
			
		||||
          description: Includes the Custom Profile Attributes information if set to true.
 | 
			
		||||
          required: false
 | 
			
		||||
          schema:
 | 
			
		||||
            type: boolean
 | 
			
		||||
      responses:
 | 
			
		||||
        "200":
 | 
			
		||||
          description: User retrieval successful
 | 
			
		||||
 
 | 
			
		||||
@@ -151,6 +151,11 @@ type Routes struct {
 | 
			
		||||
 | 
			
		||||
	OutgoingOAuthConnections *mux.Router // 'api/v4/oauth/outgoing_connections'
 | 
			
		||||
	OutgoingOAuthConnection  *mux.Router // 'api/v4/oauth/outgoing_connections/{outgoing_oauth_connection_id:[A-Za-z0-9]+}'
 | 
			
		||||
 | 
			
		||||
	CustomProfileAttributes       *mux.Router // 'api/v4/custom_profile_attributes'
 | 
			
		||||
	CustomProfileAttributesFields *mux.Router // 'api/v4/custom_profile_attributes/fields'
 | 
			
		||||
	CustomProfileAttributesField  *mux.Router // 'api/v4/custom_profile_attributes/fields/{field_id:[A-Za-z0-9]+}'
 | 
			
		||||
	CustomProfileAttributesValues *mux.Router // 'api/v4/custom_profile_attributes/values'
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type API struct {
 | 
			
		||||
@@ -288,6 +293,11 @@ func Init(srv *app.Server) (*API, error) {
 | 
			
		||||
	api.BaseRoutes.OutgoingOAuthConnections = api.BaseRoutes.APIRoot.PathPrefix("/oauth/outgoing_connections").Subrouter()
 | 
			
		||||
	api.BaseRoutes.OutgoingOAuthConnection = api.BaseRoutes.OutgoingOAuthConnections.PathPrefix("/{outgoing_oauth_connection_id:[A-Za-z0-9]+}").Subrouter()
 | 
			
		||||
 | 
			
		||||
	api.BaseRoutes.CustomProfileAttributes = api.BaseRoutes.APIRoot.PathPrefix("/custom_profile_attributes").Subrouter()
 | 
			
		||||
	api.BaseRoutes.CustomProfileAttributesFields = api.BaseRoutes.CustomProfileAttributes.PathPrefix("/fields").Subrouter()
 | 
			
		||||
	api.BaseRoutes.CustomProfileAttributesField = api.BaseRoutes.CustomProfileAttributesFields.PathPrefix("/{field_id:[A-Za-z0-9]+}").Subrouter()
 | 
			
		||||
	api.BaseRoutes.CustomProfileAttributesValues = api.BaseRoutes.CustomProfileAttributes.PathPrefix("/values").Subrouter()
 | 
			
		||||
 | 
			
		||||
	api.InitUser()
 | 
			
		||||
	api.InitBot()
 | 
			
		||||
	api.InitTeam()
 | 
			
		||||
@@ -338,6 +348,7 @@ func Init(srv *app.Server) (*API, error) {
 | 
			
		||||
	api.InitOutgoingOAuthConnection()
 | 
			
		||||
	api.InitClientPerformanceMetrics()
 | 
			
		||||
	api.InitScheduledPost()
 | 
			
		||||
	api.InitCustomProfileAttributes()
 | 
			
		||||
 | 
			
		||||
	// If we allow testing then listen for manual testing URL hits
 | 
			
		||||
	if *srv.Config().ServiceSettings.EnableTesting {
 | 
			
		||||
@@ -420,6 +431,11 @@ func InitLocal(srv *app.Server) *API {
 | 
			
		||||
 | 
			
		||||
	api.BaseRoutes.SAML = api.BaseRoutes.APIRoot.PathPrefix("/saml").Subrouter()
 | 
			
		||||
 | 
			
		||||
	api.BaseRoutes.CustomProfileAttributes = api.BaseRoutes.APIRoot.PathPrefix("/custom_profile_attributes").Subrouter()
 | 
			
		||||
	api.BaseRoutes.CustomProfileAttributesFields = api.BaseRoutes.CustomProfileAttributes.PathPrefix("/fields").Subrouter()
 | 
			
		||||
	api.BaseRoutes.CustomProfileAttributesField = api.BaseRoutes.CustomProfileAttributesFields.PathPrefix("/{field_id:[A-Za-z0-9]+}").Subrouter()
 | 
			
		||||
	api.BaseRoutes.CustomProfileAttributesValues = api.BaseRoutes.CustomProfileAttributes.PathPrefix("/values").Subrouter()
 | 
			
		||||
 | 
			
		||||
	api.InitUserLocal()
 | 
			
		||||
	api.InitTeamLocal()
 | 
			
		||||
	api.InitChannelLocal()
 | 
			
		||||
@@ -440,6 +456,7 @@ func InitLocal(srv *app.Server) *API {
 | 
			
		||||
	api.InitExportLocal()
 | 
			
		||||
	api.InitJobLocal()
 | 
			
		||||
	api.InitSamlLocal()
 | 
			
		||||
	api.InitCustomProfileAttributesLocal()
 | 
			
		||||
 | 
			
		||||
	srv.LocalRouter.Handle("/api/v4/{anything:.*}", http.HandlerFunc(api.Handle404))
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										218
									
								
								server/channels/api4/custom_profile_attributes.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										218
									
								
								server/channels/api4/custom_profile_attributes.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,218 @@
 | 
			
		||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
 | 
			
		||||
// See LICENSE.txt for license information.
 | 
			
		||||
 | 
			
		||||
package api4
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"encoding/json"
 | 
			
		||||
	"net/http"
 | 
			
		||||
	"strings"
 | 
			
		||||
 | 
			
		||||
	"github.com/mattermost/mattermost/server/public/model"
 | 
			
		||||
	"github.com/mattermost/mattermost/server/public/shared/mlog"
 | 
			
		||||
	"github.com/mattermost/mattermost/server/v8/channels/audit"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func (api *API) InitCustomProfileAttributes() {
 | 
			
		||||
	if api.srv.Config().FeatureFlags.CustomProfileAttributes {
 | 
			
		||||
		api.BaseRoutes.CustomProfileAttributesFields.Handle("", api.APISessionRequired(listCPAFields)).Methods(http.MethodGet)
 | 
			
		||||
		api.BaseRoutes.CustomProfileAttributesFields.Handle("", api.APISessionRequired(createCPAField)).Methods(http.MethodPost)
 | 
			
		||||
		api.BaseRoutes.CustomProfileAttributesField.Handle("", api.APISessionRequired(patchCPAField)).Methods(http.MethodPatch)
 | 
			
		||||
		api.BaseRoutes.CustomProfileAttributesField.Handle("", api.APISessionRequired(deleteCPAField)).Methods(http.MethodDelete)
 | 
			
		||||
		api.BaseRoutes.User.Handle("/custom_profile_attributes", api.APISessionRequired(listCPAValues)).Methods(http.MethodGet)
 | 
			
		||||
		api.BaseRoutes.CustomProfileAttributesValues.Handle("", api.APISessionRequired(patchCPAValues)).Methods(http.MethodPatch)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func listCPAFields(c *Context, w http.ResponseWriter, r *http.Request) {
 | 
			
		||||
	fields, appErr := c.App.ListCPAFields()
 | 
			
		||||
	if appErr != nil {
 | 
			
		||||
		c.Err = appErr
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if err := json.NewEncoder(w).Encode(fields); err != nil {
 | 
			
		||||
		c.Logger.Warn("Error while writing response", mlog.Err(err))
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func createCPAField(c *Context, w http.ResponseWriter, r *http.Request) {
 | 
			
		||||
	if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageSystem) {
 | 
			
		||||
		c.SetPermissionError(model.PermissionManageSystem)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	var pf *model.PropertyField
 | 
			
		||||
	err := json.NewDecoder(r.Body).Decode(&pf)
 | 
			
		||||
	if err != nil || pf == nil {
 | 
			
		||||
		c.SetInvalidParamWithErr("property_field", err)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	pf.SanitizeInput()
 | 
			
		||||
 | 
			
		||||
	auditRec := c.MakeAuditRecord("createCPAField", audit.Fail)
 | 
			
		||||
	defer c.LogAuditRec(auditRec)
 | 
			
		||||
	audit.AddEventParameterAuditable(auditRec, "property_field", pf)
 | 
			
		||||
 | 
			
		||||
	createdField, appErr := c.App.CreateCPAField(pf)
 | 
			
		||||
	if appErr != nil {
 | 
			
		||||
		c.Err = appErr
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	auditRec.Success()
 | 
			
		||||
	auditRec.AddEventResultState(createdField)
 | 
			
		||||
	auditRec.AddEventObjectType("property_field")
 | 
			
		||||
 | 
			
		||||
	w.WriteHeader(http.StatusCreated)
 | 
			
		||||
	if err := json.NewEncoder(w).Encode(createdField); err != nil {
 | 
			
		||||
		c.Logger.Warn("Error while writing response", mlog.Err(err))
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func patchCPAField(c *Context, w http.ResponseWriter, r *http.Request) {
 | 
			
		||||
	if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageSystem) {
 | 
			
		||||
		c.SetPermissionError(model.PermissionManageSystem)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	c.RequireFieldId()
 | 
			
		||||
	if c.Err != nil {
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	var patch *model.PropertyFieldPatch
 | 
			
		||||
	err := json.NewDecoder(r.Body).Decode(&patch)
 | 
			
		||||
	if err != nil || patch == nil {
 | 
			
		||||
		c.SetInvalidParamWithErr("property_field_patch", err)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	patch.SanitizeInput()
 | 
			
		||||
 | 
			
		||||
	auditRec := c.MakeAuditRecord("patchCPAField", audit.Fail)
 | 
			
		||||
	defer c.LogAuditRec(auditRec)
 | 
			
		||||
	audit.AddEventParameterAuditable(auditRec, "property_field_patch", patch)
 | 
			
		||||
 | 
			
		||||
	originalField, appErr := c.App.GetCPAField(c.Params.FieldId)
 | 
			
		||||
	if appErr != nil {
 | 
			
		||||
		c.Err = appErr
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	auditRec.AddEventPriorState(originalField)
 | 
			
		||||
 | 
			
		||||
	patchedField, appErr := c.App.PatchCPAField(c.Params.FieldId, patch)
 | 
			
		||||
	if appErr != nil {
 | 
			
		||||
		c.Err = appErr
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	auditRec.Success()
 | 
			
		||||
	auditRec.AddEventResultState(patchedField)
 | 
			
		||||
	auditRec.AddEventObjectType("property_field")
 | 
			
		||||
 | 
			
		||||
	if err := json.NewEncoder(w).Encode(patchedField); err != nil {
 | 
			
		||||
		c.Logger.Warn("Error while writing response", mlog.Err(err))
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func deleteCPAField(c *Context, w http.ResponseWriter, r *http.Request) {
 | 
			
		||||
	if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionManageSystem) {
 | 
			
		||||
		c.SetPermissionError(model.PermissionManageSystem)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	c.RequireFieldId()
 | 
			
		||||
	if c.Err != nil {
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	auditRec := c.MakeAuditRecord("deleteCPAField", audit.Fail)
 | 
			
		||||
	defer c.LogAuditRec(auditRec)
 | 
			
		||||
	audit.AddEventParameter(auditRec, "field_id", c.Params.FieldId)
 | 
			
		||||
 | 
			
		||||
	field, appErr := c.App.GetCPAField(c.Params.FieldId)
 | 
			
		||||
	if appErr != nil {
 | 
			
		||||
		c.Err = appErr
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	auditRec.AddEventPriorState(field)
 | 
			
		||||
 | 
			
		||||
	if appErr := c.App.DeleteCPAField(c.Params.FieldId); appErr != nil {
 | 
			
		||||
		c.Err = appErr
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	auditRec.Success()
 | 
			
		||||
	auditRec.AddEventResultState(field)
 | 
			
		||||
	auditRec.AddEventObjectType("property_field")
 | 
			
		||||
 | 
			
		||||
	ReturnStatusOK(w)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func patchCPAValues(c *Context, w http.ResponseWriter, r *http.Request) {
 | 
			
		||||
	var attributeValues map[string]string
 | 
			
		||||
	if jsonErr := json.NewDecoder(r.Body).Decode(&attributeValues); jsonErr != nil {
 | 
			
		||||
		c.SetInvalidParamWithErr("attrs", jsonErr)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// This check is unnecessary for now
 | 
			
		||||
	// Will be required when/if admins can patch other's values
 | 
			
		||||
	userID := c.AppContext.Session().UserId
 | 
			
		||||
	if !c.App.SessionHasPermissionToUser(*c.AppContext.Session(), userID) {
 | 
			
		||||
		c.SetPermissionError(model.PermissionEditOtherUsers)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	auditRec := c.MakeAuditRecord("patchCPAValues", audit.Fail)
 | 
			
		||||
	defer c.LogAuditRec(auditRec)
 | 
			
		||||
	audit.AddEventParameter(auditRec, "user_id", userID)
 | 
			
		||||
 | 
			
		||||
	results := make(map[string]string)
 | 
			
		||||
	for fieldID, value := range attributeValues {
 | 
			
		||||
		patchedValue, appErr := c.App.PatchCPAValue(userID, fieldID, strings.TrimSpace(value))
 | 
			
		||||
		if appErr != nil {
 | 
			
		||||
			c.Err = appErr
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
		results[fieldID] = patchedValue.Value
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	auditRec.Success()
 | 
			
		||||
	auditRec.AddEventObjectType("patchCPAValues")
 | 
			
		||||
 | 
			
		||||
	if err := json.NewEncoder(w).Encode(results); err != nil {
 | 
			
		||||
		c.Logger.Warn("Error while writing response", mlog.Err(err))
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func listCPAValues(c *Context, w http.ResponseWriter, r *http.Request) {
 | 
			
		||||
	c.RequireUserId()
 | 
			
		||||
	if c.Err != nil {
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	userID := c.Params.UserId
 | 
			
		||||
	canSee, err := c.App.UserCanSeeOtherUser(c.AppContext, c.AppContext.Session().UserId, userID)
 | 
			
		||||
	if err != nil || !canSee {
 | 
			
		||||
		c.SetPermissionError(model.PermissionViewMembers)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	values, appErr := c.App.ListCPAValues(userID)
 | 
			
		||||
	if appErr != nil {
 | 
			
		||||
		c.Err = appErr
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	returnValue := make(map[string]string)
 | 
			
		||||
	for _, value := range values {
 | 
			
		||||
		returnValue[value.FieldID] = value.Value
 | 
			
		||||
	}
 | 
			
		||||
	if err := json.NewEncoder(w).Encode(returnValue); err != nil {
 | 
			
		||||
		c.Logger.Warn("Error while writing response", mlog.Err(err))
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										17
									
								
								server/channels/api4/custom_profile_attributes_local.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								server/channels/api4/custom_profile_attributes_local.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,17 @@
 | 
			
		||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
 | 
			
		||||
// See LICENSE.txt for license information.
 | 
			
		||||
 | 
			
		||||
package api4
 | 
			
		||||
 | 
			
		||||
import "net/http"
 | 
			
		||||
 | 
			
		||||
func (api *API) InitCustomProfileAttributesLocal() {
 | 
			
		||||
	if api.srv.Config().FeatureFlags.CustomProfileAttributes {
 | 
			
		||||
		api.BaseRoutes.CustomProfileAttributesFields.Handle("", api.APILocal(listCPAFields)).Methods(http.MethodGet)
 | 
			
		||||
		api.BaseRoutes.CustomProfileAttributesFields.Handle("", api.APILocal(createCPAField)).Methods(http.MethodPost)
 | 
			
		||||
		api.BaseRoutes.CustomProfileAttributesField.Handle("", api.APILocal(patchCPAField)).Methods(http.MethodPatch)
 | 
			
		||||
		api.BaseRoutes.CustomProfileAttributesField.Handle("", api.APILocal(deleteCPAField)).Methods(http.MethodDelete)
 | 
			
		||||
		api.BaseRoutes.User.Handle("/custom_profile_attributes", api.APISessionRequired(listCPAValues)).Methods(http.MethodGet)
 | 
			
		||||
		api.BaseRoutes.CustomProfileAttributesValues.Handle("", api.APISessionRequired(patchCPAValues)).Methods(http.MethodPatch)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										269
									
								
								server/channels/api4/custom_profile_attributes_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										269
									
								
								server/channels/api4/custom_profile_attributes_test.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,269 @@
 | 
			
		||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
 | 
			
		||||
// See LICENSE.txt for license information.
 | 
			
		||||
 | 
			
		||||
package api4
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"context"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"os"
 | 
			
		||||
	"testing"
 | 
			
		||||
 | 
			
		||||
	"github.com/mattermost/mattermost/server/public/model"
 | 
			
		||||
	"github.com/stretchr/testify/require"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func TestCreateCPAField(t *testing.T) {
 | 
			
		||||
	os.Setenv("MM_FEATUREFLAGS_CUSTOMPROFILEATTRIBUTES", "true")
 | 
			
		||||
	defer os.Unsetenv("MM_FEATUREFLAGS_CUSTOMPROFILEATTRIBUTES")
 | 
			
		||||
	th := Setup(t)
 | 
			
		||||
	defer th.TearDown()
 | 
			
		||||
 | 
			
		||||
	t.Run("a user without admin permissions should not be able to create a field", func(t *testing.T) {
 | 
			
		||||
		field := &model.PropertyField{
 | 
			
		||||
			Name: model.NewId(),
 | 
			
		||||
			Type: model.PropertyFieldTypeText,
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		_, resp, err := th.Client.CreateCPAField(context.Background(), field)
 | 
			
		||||
		CheckForbiddenStatus(t, resp)
 | 
			
		||||
		require.Error(t, err)
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
	th.TestForSystemAdminAndLocal(t, func(t *testing.T, client *model.Client4) {
 | 
			
		||||
		field := &model.PropertyField{Name: model.NewId()}
 | 
			
		||||
 | 
			
		||||
		createdField, resp, err := client.CreateCPAField(context.Background(), field)
 | 
			
		||||
		CheckBadRequestStatus(t, resp)
 | 
			
		||||
		require.Error(t, err)
 | 
			
		||||
		require.Empty(t, createdField)
 | 
			
		||||
	}, "an invalid field should be rejected")
 | 
			
		||||
 | 
			
		||||
	th.TestForSystemAdminAndLocal(t, func(t *testing.T, client *model.Client4) {
 | 
			
		||||
		name := model.NewId()
 | 
			
		||||
		field := &model.PropertyField{
 | 
			
		||||
			Name:  fmt.Sprintf("  %s\t", name), // name should be sanitized
 | 
			
		||||
			Type:  model.PropertyFieldTypeText,
 | 
			
		||||
			Attrs: map[string]any{"visibility": "default"},
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		createdField, resp, err := client.CreateCPAField(context.Background(), field)
 | 
			
		||||
		CheckCreatedStatus(t, resp)
 | 
			
		||||
		require.NoError(t, err)
 | 
			
		||||
		require.NotZero(t, createdField.ID)
 | 
			
		||||
		require.Equal(t, name, createdField.Name)
 | 
			
		||||
		require.Equal(t, "default", createdField.Attrs["visibility"])
 | 
			
		||||
	}, "a user with admin permissions should be able to create the field")
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestListCPAFields(t *testing.T) {
 | 
			
		||||
	os.Setenv("MM_FEATUREFLAGS_CUSTOMPROFILEATTRIBUTES", "true")
 | 
			
		||||
	defer os.Unsetenv("MM_FEATUREFLAGS_CUSTOMPROFILEATTRIBUTES")
 | 
			
		||||
	th := Setup(t)
 | 
			
		||||
	defer th.TearDown()
 | 
			
		||||
 | 
			
		||||
	field := &model.PropertyField{
 | 
			
		||||
		Name:  model.NewId(),
 | 
			
		||||
		Type:  model.PropertyFieldTypeText,
 | 
			
		||||
		Attrs: map[string]any{"visibility": "default"},
 | 
			
		||||
	}
 | 
			
		||||
	createdField, _, err := th.SystemAdminClient.CreateCPAField(context.Background(), field)
 | 
			
		||||
	require.NoError(t, err)
 | 
			
		||||
	require.NotNil(t, createdField)
 | 
			
		||||
 | 
			
		||||
	t.Run("any user should be able to list fields", func(t *testing.T) {
 | 
			
		||||
		fields, resp, err := th.Client.ListCPAFields(context.Background())
 | 
			
		||||
		CheckOKStatus(t, resp)
 | 
			
		||||
		require.NoError(t, err)
 | 
			
		||||
		require.NotEmpty(t, fields)
 | 
			
		||||
		require.Len(t, fields, 1)
 | 
			
		||||
		require.Equal(t, createdField.ID, fields[0].ID)
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
	t.Run("the endpoint should only list non deleted fields", func(t *testing.T) {
 | 
			
		||||
		require.Nil(t, th.App.DeleteCPAField(createdField.ID))
 | 
			
		||||
		fields, resp, err := th.Client.ListCPAFields(context.Background())
 | 
			
		||||
		CheckOKStatus(t, resp)
 | 
			
		||||
		require.NoError(t, err)
 | 
			
		||||
		require.Empty(t, fields)
 | 
			
		||||
	})
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestPatchCPAField(t *testing.T) {
 | 
			
		||||
	os.Setenv("MM_FEATUREFLAGS_CUSTOMPROFILEATTRIBUTES", "true")
 | 
			
		||||
	defer os.Unsetenv("MM_FEATUREFLAGS_CUSTOMPROFILEATTRIBUTES")
 | 
			
		||||
	th := Setup(t)
 | 
			
		||||
	defer th.TearDown()
 | 
			
		||||
 | 
			
		||||
	t.Run("a user without admin permissions should not be able to patch a field", func(t *testing.T) {
 | 
			
		||||
		field := &model.PropertyField{
 | 
			
		||||
			Name: model.NewId(),
 | 
			
		||||
			Type: model.PropertyFieldTypeText,
 | 
			
		||||
		}
 | 
			
		||||
		createdField, appErr := th.App.CreateCPAField(field)
 | 
			
		||||
		require.Nil(t, appErr)
 | 
			
		||||
		require.NotNil(t, createdField)
 | 
			
		||||
 | 
			
		||||
		patch := &model.PropertyFieldPatch{Name: model.NewPointer(model.NewId())}
 | 
			
		||||
		_, resp, err := th.Client.PatchCPAField(context.Background(), createdField.ID, patch)
 | 
			
		||||
		CheckForbiddenStatus(t, resp)
 | 
			
		||||
		require.Error(t, err)
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
	th.TestForSystemAdminAndLocal(t, func(t *testing.T, client *model.Client4) {
 | 
			
		||||
		field := &model.PropertyField{
 | 
			
		||||
			Name: model.NewId(),
 | 
			
		||||
			Type: model.PropertyFieldTypeText,
 | 
			
		||||
		}
 | 
			
		||||
		createdField, appErr := th.App.CreateCPAField(field)
 | 
			
		||||
		require.Nil(t, appErr)
 | 
			
		||||
		require.NotNil(t, createdField)
 | 
			
		||||
 | 
			
		||||
		newName := model.NewId()
 | 
			
		||||
		patch := &model.PropertyFieldPatch{Name: model.NewPointer(fmt.Sprintf("  %s \t ", newName))} // name should be sanitized
 | 
			
		||||
		patchedField, resp, err := client.PatchCPAField(context.Background(), createdField.ID, patch)
 | 
			
		||||
		CheckOKStatus(t, resp)
 | 
			
		||||
		require.NoError(t, err)
 | 
			
		||||
		require.Equal(t, newName, patchedField.Name)
 | 
			
		||||
	}, "a user with admin permissions should be able to patch the field")
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestDeleteCPAField(t *testing.T) {
 | 
			
		||||
	os.Setenv("MM_FEATUREFLAGS_CUSTOMPROFILEATTRIBUTES", "true")
 | 
			
		||||
	defer os.Unsetenv("MM_FEATUREFLAGS_CUSTOMPROFILEATTRIBUTES")
 | 
			
		||||
	th := Setup(t)
 | 
			
		||||
	defer th.TearDown()
 | 
			
		||||
 | 
			
		||||
	t.Run("a user without admin permissions should not be able to delete a field", func(t *testing.T) {
 | 
			
		||||
		field := &model.PropertyField{
 | 
			
		||||
			Name: model.NewId(),
 | 
			
		||||
			Type: model.PropertyFieldTypeText,
 | 
			
		||||
		}
 | 
			
		||||
		createdField, _, err := th.SystemAdminClient.CreateCPAField(context.Background(), field)
 | 
			
		||||
		require.NoError(t, err)
 | 
			
		||||
		require.NotNil(t, createdField)
 | 
			
		||||
 | 
			
		||||
		resp, err := th.Client.DeleteCPAField(context.Background(), createdField.ID)
 | 
			
		||||
		CheckForbiddenStatus(t, resp)
 | 
			
		||||
		require.Error(t, err)
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
	th.TestForSystemAdminAndLocal(t, func(t *testing.T, client *model.Client4) {
 | 
			
		||||
		field := &model.PropertyField{
 | 
			
		||||
			Name: model.NewId(),
 | 
			
		||||
			Type: model.PropertyFieldTypeText,
 | 
			
		||||
		}
 | 
			
		||||
		createdField, _, err := th.SystemAdminClient.CreateCPAField(context.Background(), field)
 | 
			
		||||
		require.NoError(t, err)
 | 
			
		||||
		require.NotNil(t, createdField)
 | 
			
		||||
		require.Zero(t, createdField.DeleteAt)
 | 
			
		||||
 | 
			
		||||
		resp, err := client.DeleteCPAField(context.Background(), createdField.ID)
 | 
			
		||||
		CheckOKStatus(t, resp)
 | 
			
		||||
		require.NoError(t, err)
 | 
			
		||||
 | 
			
		||||
		deletedField, appErr := th.App.GetCPAField(createdField.ID)
 | 
			
		||||
		require.Nil(t, appErr)
 | 
			
		||||
		require.NotZero(t, deletedField.DeleteAt)
 | 
			
		||||
	}, "a user with admin permissions should be able to delete the field")
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestListCPAValues(t *testing.T) {
 | 
			
		||||
	os.Setenv("MM_FEATUREFLAGS_CUSTOMPROFILEATTRIBUTES", "true")
 | 
			
		||||
	defer os.Unsetenv("MM_FEATUREFLAGS_CUSTOMPROFILEATTRIBUTES")
 | 
			
		||||
	th := Setup(t).InitBasic()
 | 
			
		||||
	defer th.TearDown()
 | 
			
		||||
 | 
			
		||||
	th.RemovePermissionFromRole(model.PermissionViewMembers.Id, model.SystemUserRoleId)
 | 
			
		||||
	defer func() {
 | 
			
		||||
		th.AddPermissionToRole(model.PermissionViewMembers.Id, model.SystemUserRoleId)
 | 
			
		||||
	}()
 | 
			
		||||
 | 
			
		||||
	field := &model.PropertyField{
 | 
			
		||||
		Name: model.NewId(),
 | 
			
		||||
		Type: model.PropertyFieldTypeText,
 | 
			
		||||
	}
 | 
			
		||||
	createdField, appErr := th.App.CreateCPAField(field)
 | 
			
		||||
	require.Nil(t, appErr)
 | 
			
		||||
	require.NotNil(t, createdField)
 | 
			
		||||
 | 
			
		||||
	values := map[string]string{}
 | 
			
		||||
	values[createdField.ID] = "Field Value"
 | 
			
		||||
	_, _, err := th.Client.PatchCPAValues(context.Background(), values)
 | 
			
		||||
	require.NoError(t, err)
 | 
			
		||||
 | 
			
		||||
	// login with Client2 from this point on
 | 
			
		||||
	th.LoginBasic2()
 | 
			
		||||
 | 
			
		||||
	t.Run("any team member should be able to list values", func(t *testing.T) {
 | 
			
		||||
		values, resp, err := th.Client.ListCPAValues(context.Background(), th.BasicUser.Id)
 | 
			
		||||
		CheckOKStatus(t, resp)
 | 
			
		||||
		require.NoError(t, err)
 | 
			
		||||
		require.NotEmpty(t, values)
 | 
			
		||||
		require.Len(t, values, 1)
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
	t.Run("non team member should NOT be able to list values", func(t *testing.T) {
 | 
			
		||||
		resp, err := th.SystemAdminClient.RemoveTeamMember(context.Background(), th.BasicTeam.Id, th.BasicUser2.Id)
 | 
			
		||||
		CheckOKStatus(t, resp)
 | 
			
		||||
		require.NoError(t, err)
 | 
			
		||||
 | 
			
		||||
		_, resp, err = th.Client.ListCPAValues(context.Background(), th.BasicUser.Id)
 | 
			
		||||
		CheckForbiddenStatus(t, resp)
 | 
			
		||||
		require.Error(t, err)
 | 
			
		||||
	})
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestPatchCPAValues(t *testing.T) {
 | 
			
		||||
	os.Setenv("MM_FEATUREFLAGS_CUSTOMPROFILEATTRIBUTES", "true")
 | 
			
		||||
	defer os.Unsetenv("MM_FEATUREFLAGS_CUSTOMPROFILEATTRIBUTES")
 | 
			
		||||
	th := Setup(t).InitBasic()
 | 
			
		||||
	defer th.TearDown()
 | 
			
		||||
 | 
			
		||||
	field := &model.PropertyField{
 | 
			
		||||
		Name: model.NewId(),
 | 
			
		||||
		Type: model.PropertyFieldTypeText,
 | 
			
		||||
	}
 | 
			
		||||
	createdField, appErr := th.App.CreateCPAField(field)
 | 
			
		||||
	require.Nil(t, appErr)
 | 
			
		||||
	require.NotNil(t, createdField)
 | 
			
		||||
 | 
			
		||||
	t.Run("any team member should be able to create their own values", func(t *testing.T) {
 | 
			
		||||
		values := map[string]string{}
 | 
			
		||||
		value := "Field Value"
 | 
			
		||||
		values[createdField.ID] = fmt.Sprintf("  %s ", value) // value should be sanitized
 | 
			
		||||
		patchedValues, resp, err := th.Client.PatchCPAValues(context.Background(), values)
 | 
			
		||||
		CheckOKStatus(t, resp)
 | 
			
		||||
		require.NoError(t, err)
 | 
			
		||||
		require.NotEmpty(t, patchedValues)
 | 
			
		||||
		require.Len(t, patchedValues, 1)
 | 
			
		||||
		require.Equal(t, value, patchedValues[createdField.ID])
 | 
			
		||||
 | 
			
		||||
		values, resp, err = th.Client.ListCPAValues(context.Background(), th.BasicUser.Id)
 | 
			
		||||
		CheckOKStatus(t, resp)
 | 
			
		||||
		require.NoError(t, err)
 | 
			
		||||
		require.NotEmpty(t, values)
 | 
			
		||||
		require.Len(t, values, 1)
 | 
			
		||||
		require.Equal(t, "Field Value", values[createdField.ID])
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
	t.Run("any team member should be able to patch their own values", func(t *testing.T) {
 | 
			
		||||
		values, resp, err := th.Client.ListCPAValues(context.Background(), th.BasicUser.Id)
 | 
			
		||||
		CheckOKStatus(t, resp)
 | 
			
		||||
		require.NoError(t, err)
 | 
			
		||||
		require.NotEmpty(t, values)
 | 
			
		||||
		require.Len(t, values, 1)
 | 
			
		||||
 | 
			
		||||
		value := "Updated Field Value"
 | 
			
		||||
		values[createdField.ID] = fmt.Sprintf(" %s  \t", value) // value should be sanitized
 | 
			
		||||
		patchedValues, resp, err := th.Client.PatchCPAValues(context.Background(), values)
 | 
			
		||||
		CheckOKStatus(t, resp)
 | 
			
		||||
		require.NoError(t, err)
 | 
			
		||||
		require.Equal(t, value, patchedValues[createdField.ID])
 | 
			
		||||
 | 
			
		||||
		values, resp, err = th.Client.ListCPAValues(context.Background(), th.BasicUser.Id)
 | 
			
		||||
		CheckOKStatus(t, resp)
 | 
			
		||||
		require.NoError(t, err)
 | 
			
		||||
		require.Equal(t, value, values[createdField.ID])
 | 
			
		||||
	})
 | 
			
		||||
}
 | 
			
		||||
@@ -513,6 +513,7 @@ type AppIface interface {
 | 
			
		||||
	CountNotification(notificationType model.NotificationType, platform string)
 | 
			
		||||
	CountNotificationAck(notificationType model.NotificationType, platform string)
 | 
			
		||||
	CountNotificationReason(notificationStatus model.NotificationStatus, notificationType model.NotificationType, notificationReason model.NotificationReason, platform string)
 | 
			
		||||
	CreateCPAField(field *model.PropertyField) (*model.PropertyField, *model.AppError)
 | 
			
		||||
	CreateChannel(c request.CTX, channel *model.Channel, addMember bool) (*model.Channel, *model.AppError)
 | 
			
		||||
	CreateChannelBookmark(c request.CTX, newBookmark *model.ChannelBookmark, connectionId string) (*model.ChannelBookmarkWithFileInfo, *model.AppError)
 | 
			
		||||
	CreateChannelWithUser(c request.CTX, channel *model.Channel, userID string) (*model.Channel, *model.AppError)
 | 
			
		||||
@@ -560,6 +561,7 @@ type AppIface interface {
 | 
			
		||||
	DeleteAllExpiredPluginKeys() *model.AppError
 | 
			
		||||
	DeleteAllKeysForPlugin(pluginID string) *model.AppError
 | 
			
		||||
	DeleteBrandImage(rctx request.CTX) *model.AppError
 | 
			
		||||
	DeleteCPAField(id string) *model.AppError
 | 
			
		||||
	DeleteChannel(c request.CTX, channel *model.Channel, userID string) *model.AppError
 | 
			
		||||
	DeleteChannelBookmark(bookmarkId, connectionId string) (*model.ChannelBookmarkWithFileInfo, *model.AppError)
 | 
			
		||||
	DeleteCommand(commandID string) *model.AppError
 | 
			
		||||
@@ -643,6 +645,8 @@ type AppIface interface {
 | 
			
		||||
	GetBookmark(bookmarkId string, includeDeleted bool) (*model.ChannelBookmarkWithFileInfo, *model.AppError)
 | 
			
		||||
	GetBrandImage(rctx request.CTX) ([]byte, *model.AppError)
 | 
			
		||||
	GetBulkReactionsForPosts(postIDs []string) (map[string][]*model.Reaction, *model.AppError)
 | 
			
		||||
	GetCPAField(fieldID string) (*model.PropertyField, *model.AppError)
 | 
			
		||||
	GetCPAValue(valueID string) (*model.PropertyValue, *model.AppError)
 | 
			
		||||
	GetChannel(c request.CTX, channelID string) (*model.Channel, *model.AppError)
 | 
			
		||||
	GetChannelBookmarks(channelId string, since int64) ([]*model.ChannelBookmarkWithFileInfo, *model.AppError)
 | 
			
		||||
	GetChannelByName(c request.CTX, channelName, teamID string, includeDeleted bool) (*model.Channel, *model.AppError)
 | 
			
		||||
@@ -948,6 +952,8 @@ type AppIface interface {
 | 
			
		||||
	License() *model.License
 | 
			
		||||
	LimitedClientConfig() map[string]string
 | 
			
		||||
	ListAllCommands(teamID string, T i18n.TranslateFunc) ([]*model.Command, *model.AppError)
 | 
			
		||||
	ListCPAFields() ([]*model.PropertyField, *model.AppError)
 | 
			
		||||
	ListCPAValues(userID string) ([]*model.PropertyValue, *model.AppError)
 | 
			
		||||
	ListDirectory(path string) ([]string, *model.AppError)
 | 
			
		||||
	ListDirectoryRecursively(path string) ([]string, *model.AppError)
 | 
			
		||||
	ListExportDirectory(path string) ([]string, *model.AppError)
 | 
			
		||||
@@ -973,6 +979,8 @@ type AppIface interface {
 | 
			
		||||
	OpenInteractiveDialog(c request.CTX, request model.OpenDialogRequest) *model.AppError
 | 
			
		||||
	OriginChecker() func(*http.Request) bool
 | 
			
		||||
	OutgoingOAuthConnections() einterfaces.OutgoingOAuthConnectionInterface
 | 
			
		||||
	PatchCPAField(fieldID string, patch *model.PropertyFieldPatch) (*model.PropertyField, *model.AppError)
 | 
			
		||||
	PatchCPAValue(userID string, fieldID string, value string) (*model.PropertyValue, *model.AppError)
 | 
			
		||||
	PatchChannel(c request.CTX, channel *model.Channel, patch *model.ChannelPatch, userID string) (*model.Channel, *model.AppError)
 | 
			
		||||
	PatchChannelMembersNotifyProps(c request.CTX, members []*model.ChannelMemberIdentifier, notifyProps map[string]string) ([]*model.ChannelMember, *model.AppError)
 | 
			
		||||
	PatchPost(c request.CTX, postID string, patch *model.PostPatch, patchPostOptions *model.UpdatePostOptions) (*model.Post, *model.AppError)
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										240
									
								
								server/channels/app/custom_profile_attributes.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										240
									
								
								server/channels/app/custom_profile_attributes.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,240 @@
 | 
			
		||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
 | 
			
		||||
// See LICENSE.txt for license information.
 | 
			
		||||
 | 
			
		||||
package app
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"net/http"
 | 
			
		||||
 | 
			
		||||
	"github.com/mattermost/mattermost/server/public/model"
 | 
			
		||||
	"github.com/mattermost/mattermost/server/v8/channels/store"
 | 
			
		||||
	"github.com/pkg/errors"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
const CustomProfileAttributesFieldLimit = 20
 | 
			
		||||
 | 
			
		||||
var cpaGroupID string
 | 
			
		||||
 | 
			
		||||
// ToDo: we should explore moving this to the database cache layer
 | 
			
		||||
// instead of maintaining the ID cached at the application level
 | 
			
		||||
func (a *App) cpaGroupID() (string, error) {
 | 
			
		||||
	if cpaGroupID != "" {
 | 
			
		||||
		return cpaGroupID, nil
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	cpaGroup, err := a.Srv().propertyService.RegisterPropertyGroup(model.CustomProfileAttributesPropertyGroupName)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return "", errors.Wrap(err, "cannot register Custom Profile Attributes property group")
 | 
			
		||||
	}
 | 
			
		||||
	cpaGroupID = cpaGroup.ID
 | 
			
		||||
 | 
			
		||||
	return cpaGroupID, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (a *App) GetCPAField(fieldID string) (*model.PropertyField, *model.AppError) {
 | 
			
		||||
	groupID, err := a.cpaGroupID()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, model.NewAppError("GetCPAField", "app.custom_profile_attributes.cpa_group_id.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	field, err := a.Srv().propertyService.GetPropertyField(fieldID)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, model.NewAppError("GetCPAField", "app.custom_profile_attributes.get_property_field.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if field.GroupID != groupID {
 | 
			
		||||
		return nil, model.NewAppError("GetCPAField", "app.custom_profile_attributes.property_field_not_found.app_error", nil, "", http.StatusNotFound)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return field, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (a *App) ListCPAFields() ([]*model.PropertyField, *model.AppError) {
 | 
			
		||||
	groupID, err := a.cpaGroupID()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, model.NewAppError("GetCPAFields", "app.custom_profile_attributes.cpa_group_id.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	opts := model.PropertyFieldSearchOpts{
 | 
			
		||||
		GroupID: groupID,
 | 
			
		||||
		Page:    0,
 | 
			
		||||
		PerPage: CustomProfileAttributesFieldLimit,
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	fields, err := a.Srv().propertyService.SearchPropertyFields(opts)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, model.NewAppError("GetCPAFields", "app.custom_profile_attributes.search_property_fields.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return fields, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (a *App) CreateCPAField(field *model.PropertyField) (*model.PropertyField, *model.AppError) {
 | 
			
		||||
	groupID, err := a.cpaGroupID()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, model.NewAppError("CreateCPAField", "app.custom_profile_attributes.cpa_group_id.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	existingFields, appErr := a.ListCPAFields()
 | 
			
		||||
	if appErr != nil {
 | 
			
		||||
		return nil, appErr
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if len(existingFields) >= CustomProfileAttributesFieldLimit {
 | 
			
		||||
		return nil, model.NewAppError("CreateCPAField", "app.custom_profile_attributes.limit_reached.app_error", nil, "", http.StatusUnprocessableEntity).Wrap(err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	field.GroupID = groupID
 | 
			
		||||
	newField, err := a.Srv().propertyService.CreatePropertyField(field)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		var appErr *model.AppError
 | 
			
		||||
		switch {
 | 
			
		||||
		case errors.As(err, &appErr):
 | 
			
		||||
			return nil, appErr
 | 
			
		||||
		default:
 | 
			
		||||
			return nil, model.NewAppError("CreateCPAField", "app.custom_profile_attributes.create_property_field.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return newField, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (a *App) PatchCPAField(fieldID string, patch *model.PropertyFieldPatch) (*model.PropertyField, *model.AppError) {
 | 
			
		||||
	existingField, appErr := a.GetCPAField(fieldID)
 | 
			
		||||
	if appErr != nil {
 | 
			
		||||
		return nil, appErr
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// custom profile attributes doesn't use targets
 | 
			
		||||
	patch.TargetID = nil
 | 
			
		||||
	patch.TargetType = nil
 | 
			
		||||
	existingField.Patch(patch)
 | 
			
		||||
 | 
			
		||||
	patchedField, err := a.Srv().propertyService.UpdatePropertyField(existingField)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		var nfErr *store.ErrNotFound
 | 
			
		||||
		switch {
 | 
			
		||||
		case errors.As(err, &nfErr):
 | 
			
		||||
			return nil, model.NewAppError("UpdateCPAField", "app.custom_profile_attributes.property_field_not_found.app_error", nil, "", http.StatusNotFound).Wrap(err)
 | 
			
		||||
		default:
 | 
			
		||||
			return nil, model.NewAppError("UpdateCPAField", "app.custom_profile_attributes.property_field_update.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return patchedField, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (a *App) DeleteCPAField(id string) *model.AppError {
 | 
			
		||||
	groupID, err := a.cpaGroupID()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return model.NewAppError("DeleteCPAField", "app.custom_profile_attributes.cpa_group_id.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	existingField, err := a.Srv().propertyService.GetPropertyField(id)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return model.NewAppError("DeleteCPAField", "app.custom_profile_attributes.get_property_field.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if existingField.GroupID != groupID {
 | 
			
		||||
		return model.NewAppError("DeleteCPAField", "app.custom_profile_attributes.property_field_not_found.app_error", nil, "", http.StatusNotFound)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if err := a.Srv().propertyService.DeletePropertyField(id); err != nil {
 | 
			
		||||
		var nfErr *store.ErrNotFound
 | 
			
		||||
		switch {
 | 
			
		||||
		case errors.As(err, &nfErr):
 | 
			
		||||
			return model.NewAppError("DeleteCPAField", "app.custom_profile_attributes.property_field_not_found.app_error", nil, "", http.StatusNotFound).Wrap(err)
 | 
			
		||||
		default:
 | 
			
		||||
			return model.NewAppError("DeleteCPAField", "app.custom_profile_attributes.property_field_delete.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (a *App) ListCPAValues(userID string) ([]*model.PropertyValue, *model.AppError) {
 | 
			
		||||
	groupID, err := a.cpaGroupID()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, model.NewAppError("GetCPAFields", "app.custom_profile_attributes.cpa_group_id.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	opts := model.PropertyValueSearchOpts{
 | 
			
		||||
		GroupID:        groupID,
 | 
			
		||||
		TargetID:       userID,
 | 
			
		||||
		Page:           0,
 | 
			
		||||
		PerPage:        999999,
 | 
			
		||||
		IncludeDeleted: false,
 | 
			
		||||
	}
 | 
			
		||||
	fields, err := a.Srv().propertyService.SearchPropertyValues(opts)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, model.NewAppError("ListCPAValues", "app.custom_profile_attributes.list_property_values.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return fields, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (a *App) GetCPAValue(valueID string) (*model.PropertyValue, *model.AppError) {
 | 
			
		||||
	groupID, err := a.cpaGroupID()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, model.NewAppError("GetCPAValue", "app.custom_profile_attributes.cpa_group_id.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	value, err := a.Srv().propertyService.GetPropertyValue(valueID)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, model.NewAppError("GetCPAValue", "app.custom_profile_attributes.get_property_field.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if value.GroupID != groupID {
 | 
			
		||||
		return nil, model.NewAppError("GetCPAValue", "app.custom_profile_attributes.property_field_not_found.app_error", nil, "", http.StatusNotFound)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return value, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (a *App) PatchCPAValue(userID string, fieldID string, value string) (*model.PropertyValue, *model.AppError) {
 | 
			
		||||
	groupID, err := a.cpaGroupID()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, model.NewAppError("PatchCPAValues", "app.custom_profile_attributes.cpa_group_id.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// make sure field exists in this group
 | 
			
		||||
	existingField, appErr := a.GetCPAField(fieldID)
 | 
			
		||||
	if appErr != nil {
 | 
			
		||||
		return nil, model.NewAppError("PatchCPAValue", "app.custom_profile_attributes.property_field_not_found.app_error", nil, "", http.StatusNotFound).Wrap(appErr)
 | 
			
		||||
	} else if existingField.DeleteAt > 0 {
 | 
			
		||||
		return nil, model.NewAppError("PatchCPAValue", "app.custom_profile_attributes.property_field_not_found.app_error", nil, "", http.StatusNotFound)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	existingValues, appErr := a.ListCPAValues(userID)
 | 
			
		||||
	if appErr != nil {
 | 
			
		||||
		return nil, model.NewAppError("PatchCPAValue", "app.custom_profile_attributes.property_value_list.app_error", nil, "", http.StatusNotFound).Wrap(err)
 | 
			
		||||
	}
 | 
			
		||||
	var existingValue *model.PropertyValue
 | 
			
		||||
	for key, value := range existingValues {
 | 
			
		||||
		if value.FieldID == fieldID {
 | 
			
		||||
			existingValue = existingValues[key]
 | 
			
		||||
			break
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if existingValue != nil {
 | 
			
		||||
		existingValue.Value = value
 | 
			
		||||
		_, err = a.ch.srv.propertyService.UpdatePropertyValue(existingValue)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return nil, model.NewAppError("PatchCPAValue", "app.custom_profile_attributes.property_value_update.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
 | 
			
		||||
		}
 | 
			
		||||
	} else {
 | 
			
		||||
		propertyValue := &model.PropertyValue{
 | 
			
		||||
			GroupID:    groupID,
 | 
			
		||||
			TargetType: "user",
 | 
			
		||||
			TargetID:   userID,
 | 
			
		||||
			FieldID:    fieldID,
 | 
			
		||||
			Value:      value,
 | 
			
		||||
		}
 | 
			
		||||
		existingValue, err = a.ch.srv.propertyService.CreatePropertyValue(propertyValue)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return nil, model.NewAppError("PatchCPAValue", "app.custom_profile_attributes.property_value_creation.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	return existingValue, nil
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										462
									
								
								server/channels/app/custom_profile_attributes_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										462
									
								
								server/channels/app/custom_profile_attributes_test.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,462 @@
 | 
			
		||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
 | 
			
		||||
// See LICENSE.txt for license information.
 | 
			
		||||
 | 
			
		||||
package app
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"net/http"
 | 
			
		||||
	"os"
 | 
			
		||||
	"testing"
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
	"github.com/mattermost/mattermost/server/public/model"
 | 
			
		||||
	"github.com/stretchr/testify/require"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func TestGetCPAField(t *testing.T) {
 | 
			
		||||
	os.Setenv("MM_FEATUREFLAGS_CUSTOMPROFILEATTRIBUTES", "true")
 | 
			
		||||
	defer os.Unsetenv("MM_FEATUREFLAGS_CUSTOMPROFILEATTRIBUTES")
 | 
			
		||||
	th := Setup(t).InitBasic()
 | 
			
		||||
	defer th.TearDown()
 | 
			
		||||
 | 
			
		||||
	cpaGroupID, cErr := th.App.cpaGroupID()
 | 
			
		||||
	require.NoError(t, cErr)
 | 
			
		||||
 | 
			
		||||
	t.Run("should fail when getting a non-existent field", func(t *testing.T) {
 | 
			
		||||
		field, err := th.App.GetCPAField(model.NewId())
 | 
			
		||||
		require.NotNil(t, err)
 | 
			
		||||
		require.Equal(t, "app.custom_profile_attributes.get_property_field.app_error", err.Id)
 | 
			
		||||
		require.Empty(t, field)
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
	t.Run("should fail when getting a field from a different group", func(t *testing.T) {
 | 
			
		||||
		field := &model.PropertyField{
 | 
			
		||||
			GroupID: model.NewId(),
 | 
			
		||||
			Name:    model.NewId(),
 | 
			
		||||
			Type:    model.PropertyFieldTypeText,
 | 
			
		||||
		}
 | 
			
		||||
		createdField, err := th.App.Srv().propertyService.CreatePropertyField(field)
 | 
			
		||||
		require.NoError(t, err)
 | 
			
		||||
 | 
			
		||||
		fetchedField, appErr := th.App.GetCPAField(createdField.ID)
 | 
			
		||||
		require.NotNil(t, appErr)
 | 
			
		||||
		require.Equal(t, "app.custom_profile_attributes.property_field_not_found.app_error", appErr.Id)
 | 
			
		||||
		require.Empty(t, fetchedField)
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
	t.Run("should get an existing CPA field", func(t *testing.T) {
 | 
			
		||||
		field := &model.PropertyField{
 | 
			
		||||
			GroupID: cpaGroupID,
 | 
			
		||||
			Name:    "Test Field",
 | 
			
		||||
			Type:    model.PropertyFieldTypeText,
 | 
			
		||||
			Attrs:   map[string]any{"visibility": "hidden"},
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		createdField, err := th.App.CreateCPAField(field)
 | 
			
		||||
		require.Nil(t, err)
 | 
			
		||||
		require.NotEmpty(t, createdField.ID)
 | 
			
		||||
 | 
			
		||||
		fetchedField, err := th.App.GetCPAField(createdField.ID)
 | 
			
		||||
		require.Nil(t, err)
 | 
			
		||||
		require.Equal(t, createdField.ID, fetchedField.ID)
 | 
			
		||||
		require.Equal(t, "Test Field", fetchedField.Name)
 | 
			
		||||
		require.Equal(t, map[string]any{"visibility": "hidden"}, fetchedField.Attrs)
 | 
			
		||||
	})
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestListCPAFields(t *testing.T) {
 | 
			
		||||
	os.Setenv("MM_FEATUREFLAGS_CUSTOMPROFILEATTRIBUTES", "true")
 | 
			
		||||
	defer os.Unsetenv("MM_FEATUREFLAGS_CUSTOMPROFILEATTRIBUTES")
 | 
			
		||||
	th := Setup(t).InitBasic()
 | 
			
		||||
	defer th.TearDown()
 | 
			
		||||
 | 
			
		||||
	cpaGroupID, cErr := th.App.cpaGroupID()
 | 
			
		||||
	require.NoError(t, cErr)
 | 
			
		||||
 | 
			
		||||
	t.Run("should list the CPA property fields", func(t *testing.T) {
 | 
			
		||||
		field1 := &model.PropertyField{
 | 
			
		||||
			GroupID: cpaGroupID,
 | 
			
		||||
			Name:    "Field 1",
 | 
			
		||||
			Type:    model.PropertyFieldTypeText,
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		_, err := th.App.Srv().propertyService.CreatePropertyField(field1)
 | 
			
		||||
		require.NoError(t, err)
 | 
			
		||||
 | 
			
		||||
		field2 := &model.PropertyField{
 | 
			
		||||
			GroupID: model.NewId(),
 | 
			
		||||
			Name:    "Field 2",
 | 
			
		||||
			Type:    model.PropertyFieldTypeText,
 | 
			
		||||
		}
 | 
			
		||||
		_, err = th.App.Srv().propertyService.CreatePropertyField(field2)
 | 
			
		||||
		require.NoError(t, err)
 | 
			
		||||
 | 
			
		||||
		field3 := &model.PropertyField{
 | 
			
		||||
			GroupID: cpaGroupID,
 | 
			
		||||
			Name:    "Field 3",
 | 
			
		||||
			Type:    model.PropertyFieldTypeText,
 | 
			
		||||
		}
 | 
			
		||||
		_, err = th.App.Srv().propertyService.CreatePropertyField(field3)
 | 
			
		||||
		require.NoError(t, err)
 | 
			
		||||
 | 
			
		||||
		fields, appErr := th.App.ListCPAFields()
 | 
			
		||||
		require.Nil(t, appErr)
 | 
			
		||||
		require.Len(t, fields, 2)
 | 
			
		||||
 | 
			
		||||
		fieldNames := []string{}
 | 
			
		||||
		for _, field := range fields {
 | 
			
		||||
			fieldNames = append(fieldNames, field.Name)
 | 
			
		||||
		}
 | 
			
		||||
		require.ElementsMatch(t, []string{"Field 1", "Field 3"}, fieldNames)
 | 
			
		||||
	})
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestCreateCPAField(t *testing.T) {
 | 
			
		||||
	os.Setenv("MM_FEATUREFLAGS_CUSTOMPROFILEATTRIBUTES", "true")
 | 
			
		||||
	defer os.Unsetenv("MM_FEATUREFLAGS_CUSTOMPROFILEATTRIBUTES")
 | 
			
		||||
	th := Setup(t).InitBasic()
 | 
			
		||||
 | 
			
		||||
	cpaGroupID, cErr := th.App.cpaGroupID()
 | 
			
		||||
	require.NoError(t, cErr)
 | 
			
		||||
 | 
			
		||||
	t.Run("should fail if the field is not valid", func(t *testing.T) {
 | 
			
		||||
		field := &model.PropertyField{Name: model.NewId()}
 | 
			
		||||
 | 
			
		||||
		createdField, err := th.App.CreateCPAField(field)
 | 
			
		||||
		require.NotNil(t, err)
 | 
			
		||||
		require.Empty(t, createdField)
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
	t.Run("should not be able to create a property field for a different feature", func(t *testing.T) {
 | 
			
		||||
		field := &model.PropertyField{
 | 
			
		||||
			GroupID: model.NewId(),
 | 
			
		||||
			Name:    model.NewId(),
 | 
			
		||||
			Type:    model.PropertyFieldTypeText,
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		createdField, appErr := th.App.CreateCPAField(field)
 | 
			
		||||
		require.Nil(t, appErr)
 | 
			
		||||
		require.Equal(t, cpaGroupID, createdField.GroupID)
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
	t.Run("should correctly create a CPA field", func(t *testing.T) {
 | 
			
		||||
		field := &model.PropertyField{
 | 
			
		||||
			GroupID: cpaGroupID,
 | 
			
		||||
			Name:    model.NewId(),
 | 
			
		||||
			Type:    model.PropertyFieldTypeText,
 | 
			
		||||
			Attrs:   map[string]any{"visibility": "hidden"},
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		createdField, err := th.App.CreateCPAField(field)
 | 
			
		||||
		require.Nil(t, err)
 | 
			
		||||
		require.NotZero(t, createdField.ID)
 | 
			
		||||
		require.Equal(t, cpaGroupID, createdField.GroupID)
 | 
			
		||||
		require.Equal(t, map[string]any{"visibility": "hidden"}, createdField.Attrs)
 | 
			
		||||
 | 
			
		||||
		fetchedField, gErr := th.App.Srv().propertyService.GetPropertyField(createdField.ID)
 | 
			
		||||
		require.NoError(t, gErr)
 | 
			
		||||
		require.Equal(t, field.Name, fetchedField.Name)
 | 
			
		||||
		require.NotZero(t, fetchedField.CreateAt)
 | 
			
		||||
		require.Equal(t, fetchedField.CreateAt, fetchedField.UpdateAt)
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
	// reset the server at this point to avoid polluting the state
 | 
			
		||||
	th.TearDown()
 | 
			
		||||
 | 
			
		||||
	t.Run("CPA should honor the field limit", func(t *testing.T) {
 | 
			
		||||
		th := Setup(t).InitBasic()
 | 
			
		||||
		defer th.TearDown()
 | 
			
		||||
 | 
			
		||||
		t.Run("should not be able to create CPA fields above the limit", func(t *testing.T) {
 | 
			
		||||
			// we create the rest of the fields required to reach the limit
 | 
			
		||||
			for i := 1; i <= CustomProfileAttributesFieldLimit; i++ {
 | 
			
		||||
				field := &model.PropertyField{
 | 
			
		||||
					Name: model.NewId(),
 | 
			
		||||
					Type: model.PropertyFieldTypeText,
 | 
			
		||||
				}
 | 
			
		||||
				createdField, err := th.App.CreateCPAField(field)
 | 
			
		||||
				require.Nil(t, err)
 | 
			
		||||
				require.NotZero(t, createdField.ID)
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			// then, we create a last one that would exceed the limit
 | 
			
		||||
			field := &model.PropertyField{
 | 
			
		||||
				Name: model.NewId(),
 | 
			
		||||
				Type: model.PropertyFieldTypeText,
 | 
			
		||||
			}
 | 
			
		||||
			createdField, err := th.App.CreateCPAField(field)
 | 
			
		||||
			require.NotNil(t, err)
 | 
			
		||||
			require.Equal(t, http.StatusUnprocessableEntity, err.StatusCode)
 | 
			
		||||
			require.Zero(t, createdField)
 | 
			
		||||
		})
 | 
			
		||||
 | 
			
		||||
		t.Run("deleted fields should not count for the limit", func(t *testing.T) {
 | 
			
		||||
			// we retrieve the list of fields and check we've reached the limit
 | 
			
		||||
			fields, err := th.App.ListCPAFields()
 | 
			
		||||
			require.Nil(t, err)
 | 
			
		||||
			require.Len(t, fields, CustomProfileAttributesFieldLimit)
 | 
			
		||||
 | 
			
		||||
			// then we delete one field
 | 
			
		||||
			require.Nil(t, th.App.DeleteCPAField(fields[0].ID))
 | 
			
		||||
 | 
			
		||||
			// creating a new one should work now
 | 
			
		||||
			field := &model.PropertyField{
 | 
			
		||||
				Name: model.NewId(),
 | 
			
		||||
				Type: model.PropertyFieldTypeText,
 | 
			
		||||
			}
 | 
			
		||||
			createdField, err := th.App.CreateCPAField(field)
 | 
			
		||||
			require.Nil(t, err)
 | 
			
		||||
			require.NotZero(t, createdField.ID)
 | 
			
		||||
		})
 | 
			
		||||
	})
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestPatchCPAField(t *testing.T) {
 | 
			
		||||
	os.Setenv("MM_FEATUREFLAGS_CUSTOMPROFILEATTRIBUTES", "true")
 | 
			
		||||
	defer os.Unsetenv("MM_FEATUREFLAGS_CUSTOMPROFILEATTRIBUTES")
 | 
			
		||||
	th := Setup(t).InitBasic()
 | 
			
		||||
	defer th.TearDown()
 | 
			
		||||
 | 
			
		||||
	cpaGroupID, cErr := th.App.cpaGroupID()
 | 
			
		||||
	require.NoError(t, cErr)
 | 
			
		||||
 | 
			
		||||
	newField := &model.PropertyField{
 | 
			
		||||
		GroupID: cpaGroupID,
 | 
			
		||||
		Name:    model.NewId(),
 | 
			
		||||
		Type:    model.PropertyFieldTypeText,
 | 
			
		||||
		Attrs:   map[string]any{"visibility": "hidden"},
 | 
			
		||||
	}
 | 
			
		||||
	createdField, err := th.App.CreateCPAField(newField)
 | 
			
		||||
	require.Nil(t, err)
 | 
			
		||||
 | 
			
		||||
	patch := &model.PropertyFieldPatch{
 | 
			
		||||
		Name:       model.NewPointer("Patched name"),
 | 
			
		||||
		Attrs:      model.NewPointer(map[string]any{"visibility": "default"}),
 | 
			
		||||
		TargetID:   model.NewPointer(model.NewId()),
 | 
			
		||||
		TargetType: model.NewPointer(model.NewId()),
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	t.Run("should fail if the field doesn't exist", func(t *testing.T) {
 | 
			
		||||
		updatedField, err := th.App.PatchCPAField(model.NewId(), patch)
 | 
			
		||||
		require.NotNil(t, err)
 | 
			
		||||
		require.Empty(t, updatedField)
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
	t.Run("should not allow to patch a field outside of CPA", func(t *testing.T) {
 | 
			
		||||
		newField := &model.PropertyField{
 | 
			
		||||
			GroupID: model.NewId(),
 | 
			
		||||
			Name:    model.NewId(),
 | 
			
		||||
			Type:    model.PropertyFieldTypeText,
 | 
			
		||||
		}
 | 
			
		||||
		field, err := th.App.Srv().propertyService.CreatePropertyField(newField)
 | 
			
		||||
		require.NoError(t, err)
 | 
			
		||||
 | 
			
		||||
		updatedField, uErr := th.App.PatchCPAField(field.ID, patch)
 | 
			
		||||
		require.NotNil(t, uErr)
 | 
			
		||||
		require.Equal(t, "app.custom_profile_attributes.property_field_not_found.app_error", uErr.Id)
 | 
			
		||||
		require.Empty(t, updatedField)
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
	t.Run("should correctly patch the CPA property field", func(t *testing.T) {
 | 
			
		||||
		time.Sleep(10 * time.Millisecond) // ensure the UpdateAt is different than CreateAt
 | 
			
		||||
 | 
			
		||||
		updatedField, err := th.App.PatchCPAField(createdField.ID, patch)
 | 
			
		||||
		require.Nil(t, err)
 | 
			
		||||
		require.Equal(t, createdField.ID, updatedField.ID)
 | 
			
		||||
		require.Equal(t, "Patched name", updatedField.Name)
 | 
			
		||||
		require.Equal(t, "default", updatedField.Attrs["visibility"])
 | 
			
		||||
		require.Empty(t, updatedField.TargetID, "CPA should not allow to patch the field's target ID")
 | 
			
		||||
		require.Empty(t, updatedField.TargetType, "CPA should not allow to patch the field's target type")
 | 
			
		||||
		require.Greater(t, updatedField.UpdateAt, createdField.UpdateAt)
 | 
			
		||||
	})
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestDeleteCPAField(t *testing.T) {
 | 
			
		||||
	os.Setenv("MM_FEATUREFLAGS_CUSTOMPROFILEATTRIBUTES", "true")
 | 
			
		||||
	defer os.Unsetenv("MM_FEATUREFLAGS_CUSTOMPROFILEATTRIBUTES")
 | 
			
		||||
	th := Setup(t).InitBasic()
 | 
			
		||||
	defer th.TearDown()
 | 
			
		||||
 | 
			
		||||
	cpaGroupID, cErr := th.App.cpaGroupID()
 | 
			
		||||
	require.NoError(t, cErr)
 | 
			
		||||
 | 
			
		||||
	newField := &model.PropertyField{
 | 
			
		||||
		GroupID: cpaGroupID,
 | 
			
		||||
		Name:    model.NewId(),
 | 
			
		||||
		Type:    model.PropertyFieldTypeText,
 | 
			
		||||
	}
 | 
			
		||||
	createdField, err := th.App.CreateCPAField(newField)
 | 
			
		||||
	require.Nil(t, err)
 | 
			
		||||
 | 
			
		||||
	for i := 0; i < 3; i++ {
 | 
			
		||||
		newValue := &model.PropertyValue{
 | 
			
		||||
			TargetID:   model.NewId(),
 | 
			
		||||
			TargetType: "user",
 | 
			
		||||
			GroupID:    cpaGroupID,
 | 
			
		||||
			FieldID:    createdField.ID,
 | 
			
		||||
			Value:      fmt.Sprintf("Value %d", i),
 | 
			
		||||
		}
 | 
			
		||||
		value, err := th.App.Srv().propertyService.CreatePropertyValue(newValue)
 | 
			
		||||
		require.NoError(t, err)
 | 
			
		||||
		require.NotZero(t, value.ID)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	t.Run("should fail if the field doesn't exist", func(t *testing.T) {
 | 
			
		||||
		err := th.App.DeleteCPAField(model.NewId())
 | 
			
		||||
		require.NotNil(t, err)
 | 
			
		||||
		require.Equal(t, "app.custom_profile_attributes.get_property_field.app_error", err.Id)
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
	t.Run("should not allow to delete a field outside of CPA", func(t *testing.T) {
 | 
			
		||||
		newField := &model.PropertyField{
 | 
			
		||||
			GroupID: model.NewId(),
 | 
			
		||||
			Name:    model.NewId(),
 | 
			
		||||
			Type:    model.PropertyFieldTypeText,
 | 
			
		||||
		}
 | 
			
		||||
		field, err := th.App.Srv().propertyService.CreatePropertyField(newField)
 | 
			
		||||
		require.NoError(t, err)
 | 
			
		||||
 | 
			
		||||
		dErr := th.App.DeleteCPAField(field.ID)
 | 
			
		||||
		require.NotNil(t, dErr)
 | 
			
		||||
		require.Equal(t, "app.custom_profile_attributes.property_field_not_found.app_error", dErr.Id)
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
	t.Run("should correctly delete the field", func(t *testing.T) {
 | 
			
		||||
		// check that we have the associated values to the field prior deletion
 | 
			
		||||
		opts := model.PropertyValueSearchOpts{PerPage: 10, FieldID: createdField.ID}
 | 
			
		||||
		values, err := th.App.Srv().propertyService.SearchPropertyValues(opts)
 | 
			
		||||
		require.NoError(t, err)
 | 
			
		||||
		require.Len(t, values, 3)
 | 
			
		||||
 | 
			
		||||
		// delete the field
 | 
			
		||||
		require.Nil(t, th.App.DeleteCPAField(createdField.ID))
 | 
			
		||||
 | 
			
		||||
		// check that it is marked as deleted
 | 
			
		||||
		fetchedField, err := th.App.Srv().propertyService.GetPropertyField(createdField.ID)
 | 
			
		||||
		require.NoError(t, err)
 | 
			
		||||
		require.NotZero(t, fetchedField.DeleteAt)
 | 
			
		||||
 | 
			
		||||
		// ensure that the associated fields have been marked as deleted too
 | 
			
		||||
		values, err = th.App.Srv().propertyService.SearchPropertyValues(opts)
 | 
			
		||||
		require.NoError(t, err)
 | 
			
		||||
		require.Len(t, values, 0)
 | 
			
		||||
 | 
			
		||||
		opts.IncludeDeleted = true
 | 
			
		||||
		values, err = th.App.Srv().propertyService.SearchPropertyValues(opts)
 | 
			
		||||
		require.NoError(t, err)
 | 
			
		||||
		require.Len(t, values, 3)
 | 
			
		||||
		for _, value := range values {
 | 
			
		||||
			require.NotZero(t, value.DeleteAt)
 | 
			
		||||
		}
 | 
			
		||||
	})
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestGetCPAValue(t *testing.T) {
 | 
			
		||||
	os.Setenv("MM_FEATUREFLAGS_CUSTOMPROFILEATTRIBUTES", "true")
 | 
			
		||||
	defer os.Unsetenv("MM_FEATUREFLAGS_CUSTOMPROFILEATTRIBUTES")
 | 
			
		||||
	th := Setup(t).InitBasic()
 | 
			
		||||
	defer th.TearDown()
 | 
			
		||||
 | 
			
		||||
	cpaGroupID, cErr := th.App.cpaGroupID()
 | 
			
		||||
	require.NoError(t, cErr)
 | 
			
		||||
 | 
			
		||||
	fieldID := model.NewId()
 | 
			
		||||
 | 
			
		||||
	t.Run("should fail if the value doesn't exist", func(t *testing.T) {
 | 
			
		||||
		pv, appErr := th.App.GetCPAValue(model.NewId())
 | 
			
		||||
		require.NotNil(t, appErr)
 | 
			
		||||
		require.Nil(t, pv)
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
	t.Run("should fail if the group id is invalid", func(t *testing.T) {
 | 
			
		||||
		propertyValue := &model.PropertyValue{
 | 
			
		||||
			TargetID:   model.NewId(),
 | 
			
		||||
			TargetType: "user",
 | 
			
		||||
			GroupID:    model.NewId(),
 | 
			
		||||
			FieldID:    fieldID,
 | 
			
		||||
			Value:      "Value",
 | 
			
		||||
		}
 | 
			
		||||
		propertyValue, err := th.App.Srv().propertyService.CreatePropertyValue(propertyValue)
 | 
			
		||||
		require.NoError(t, err)
 | 
			
		||||
 | 
			
		||||
		pv, appErr := th.App.GetCPAValue(propertyValue.ID)
 | 
			
		||||
		require.NotNil(t, appErr)
 | 
			
		||||
		require.Nil(t, pv)
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
	t.Run("should succeed if id exists", func(t *testing.T) {
 | 
			
		||||
		propertyValue := &model.PropertyValue{
 | 
			
		||||
			TargetID:   model.NewId(),
 | 
			
		||||
			TargetType: "user",
 | 
			
		||||
			GroupID:    cpaGroupID,
 | 
			
		||||
			FieldID:    fieldID,
 | 
			
		||||
			Value:      "Value",
 | 
			
		||||
		}
 | 
			
		||||
		propertyValue, err := th.App.Srv().propertyService.CreatePropertyValue(propertyValue)
 | 
			
		||||
		require.NoError(t, err)
 | 
			
		||||
 | 
			
		||||
		pv, appErr := th.App.GetCPAValue(propertyValue.ID)
 | 
			
		||||
		require.Nil(t, appErr)
 | 
			
		||||
		require.NotNil(t, pv)
 | 
			
		||||
	})
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestPatchCPAValue(t *testing.T) {
 | 
			
		||||
	os.Setenv("MM_FEATUREFLAGS_CUSTOMPROFILEATTRIBUTES", "true")
 | 
			
		||||
	defer os.Unsetenv("MM_FEATUREFLAGS_CUSTOMPROFILEATTRIBUTES")
 | 
			
		||||
	th := Setup(t).InitBasic()
 | 
			
		||||
	defer th.TearDown()
 | 
			
		||||
 | 
			
		||||
	cpaGroupID, cErr := th.App.cpaGroupID()
 | 
			
		||||
	require.NoError(t, cErr)
 | 
			
		||||
 | 
			
		||||
	t.Run("should fail if the field doesn't exist", func(t *testing.T) {
 | 
			
		||||
		invalidFieldID := model.NewId()
 | 
			
		||||
		_, appErr := th.App.PatchCPAValue(model.NewId(), invalidFieldID, "fieldValue")
 | 
			
		||||
		require.NotNil(t, appErr)
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
	t.Run("should create value if new field value", func(t *testing.T) {
 | 
			
		||||
		newField := &model.PropertyField{
 | 
			
		||||
			GroupID: cpaGroupID,
 | 
			
		||||
			Name:    model.NewId(),
 | 
			
		||||
			Type:    model.PropertyFieldTypeText,
 | 
			
		||||
		}
 | 
			
		||||
		createdField, err := th.App.Srv().propertyService.CreatePropertyField(newField)
 | 
			
		||||
		require.NoError(t, err)
 | 
			
		||||
 | 
			
		||||
		userID := model.NewId()
 | 
			
		||||
		patchedValue, appErr := th.App.PatchCPAValue(userID, createdField.ID, "test value")
 | 
			
		||||
		require.Nil(t, appErr)
 | 
			
		||||
		require.NotNil(t, patchedValue)
 | 
			
		||||
		require.Equal(t, "test value", patchedValue.Value)
 | 
			
		||||
		require.Equal(t, userID, patchedValue.TargetID)
 | 
			
		||||
 | 
			
		||||
		t.Run("should correctly patch the CPA property value", func(t *testing.T) {
 | 
			
		||||
			patch2, appErr := th.App.PatchCPAValue(userID, createdField.ID, "new patched value")
 | 
			
		||||
			require.Nil(t, appErr)
 | 
			
		||||
			require.NotNil(t, patch2)
 | 
			
		||||
			require.Equal(t, patchedValue.ID, patch2.ID)
 | 
			
		||||
			require.Equal(t, "new patched value", patch2.Value)
 | 
			
		||||
			require.Equal(t, userID, patch2.TargetID)
 | 
			
		||||
		})
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
	t.Run("should fail if field is deleted", func(t *testing.T) {
 | 
			
		||||
		newField := &model.PropertyField{
 | 
			
		||||
			GroupID: cpaGroupID,
 | 
			
		||||
			Name:    model.NewId(),
 | 
			
		||||
			Type:    model.PropertyFieldTypeText,
 | 
			
		||||
		}
 | 
			
		||||
		createdField, err := th.App.Srv().propertyService.CreatePropertyField(newField)
 | 
			
		||||
		require.NoError(t, err)
 | 
			
		||||
		err = th.App.Srv().propertyService.DeletePropertyField(createdField.ID)
 | 
			
		||||
		require.NoError(t, err)
 | 
			
		||||
 | 
			
		||||
		userID := model.NewId()
 | 
			
		||||
		patchedValue, appErr := th.App.PatchCPAValue(userID, createdField.ID, "test value")
 | 
			
		||||
		require.NotNil(t, appErr)
 | 
			
		||||
		require.Nil(t, patchedValue)
 | 
			
		||||
	})
 | 
			
		||||
}
 | 
			
		||||
@@ -2041,6 +2041,28 @@ func (a *OpenTracingAppLayer) CreateBot(rctx request.CTX, bot *model.Bot) (*mode
 | 
			
		||||
	return resultVar0, resultVar1
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (a *OpenTracingAppLayer) CreateCPAField(field *model.PropertyField) (*model.PropertyField, *model.AppError) {
 | 
			
		||||
	origCtx := a.ctx
 | 
			
		||||
	span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.CreateCPAField")
 | 
			
		||||
 | 
			
		||||
	a.ctx = newCtx
 | 
			
		||||
	a.app.Srv().Store().SetContext(newCtx)
 | 
			
		||||
	defer func() {
 | 
			
		||||
		a.app.Srv().Store().SetContext(origCtx)
 | 
			
		||||
		a.ctx = origCtx
 | 
			
		||||
	}()
 | 
			
		||||
 | 
			
		||||
	defer span.Finish()
 | 
			
		||||
	resultVar0, resultVar1 := a.app.CreateCPAField(field)
 | 
			
		||||
 | 
			
		||||
	if resultVar1 != nil {
 | 
			
		||||
		span.LogFields(spanlog.Error(resultVar1))
 | 
			
		||||
		ext.Error.Set(span, true)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return resultVar0, resultVar1
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (a *OpenTracingAppLayer) CreateChannel(c request.CTX, channel *model.Channel, addMember bool) (*model.Channel, *model.AppError) {
 | 
			
		||||
	origCtx := a.ctx
 | 
			
		||||
	span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.CreateChannel")
 | 
			
		||||
@@ -3206,6 +3228,28 @@ func (a *OpenTracingAppLayer) DeleteBrandImage(rctx request.CTX) *model.AppError
 | 
			
		||||
	return resultVar0
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (a *OpenTracingAppLayer) DeleteCPAField(id string) *model.AppError {
 | 
			
		||||
	origCtx := a.ctx
 | 
			
		||||
	span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.DeleteCPAField")
 | 
			
		||||
 | 
			
		||||
	a.ctx = newCtx
 | 
			
		||||
	a.app.Srv().Store().SetContext(newCtx)
 | 
			
		||||
	defer func() {
 | 
			
		||||
		a.app.Srv().Store().SetContext(origCtx)
 | 
			
		||||
		a.ctx = origCtx
 | 
			
		||||
	}()
 | 
			
		||||
 | 
			
		||||
	defer span.Finish()
 | 
			
		||||
	resultVar0 := a.app.DeleteCPAField(id)
 | 
			
		||||
 | 
			
		||||
	if resultVar0 != nil {
 | 
			
		||||
		span.LogFields(spanlog.Error(resultVar0))
 | 
			
		||||
		ext.Error.Set(span, true)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return resultVar0
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (a *OpenTracingAppLayer) DeleteChannel(c request.CTX, channel *model.Channel, userID string) *model.AppError {
 | 
			
		||||
	origCtx := a.ctx
 | 
			
		||||
	span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.DeleteChannel")
 | 
			
		||||
@@ -5484,6 +5528,50 @@ func (a *OpenTracingAppLayer) GetBulkReactionsForPosts(postIDs []string) (map[st
 | 
			
		||||
	return resultVar0, resultVar1
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (a *OpenTracingAppLayer) GetCPAField(fieldID string) (*model.PropertyField, *model.AppError) {
 | 
			
		||||
	origCtx := a.ctx
 | 
			
		||||
	span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetCPAField")
 | 
			
		||||
 | 
			
		||||
	a.ctx = newCtx
 | 
			
		||||
	a.app.Srv().Store().SetContext(newCtx)
 | 
			
		||||
	defer func() {
 | 
			
		||||
		a.app.Srv().Store().SetContext(origCtx)
 | 
			
		||||
		a.ctx = origCtx
 | 
			
		||||
	}()
 | 
			
		||||
 | 
			
		||||
	defer span.Finish()
 | 
			
		||||
	resultVar0, resultVar1 := a.app.GetCPAField(fieldID)
 | 
			
		||||
 | 
			
		||||
	if resultVar1 != nil {
 | 
			
		||||
		span.LogFields(spanlog.Error(resultVar1))
 | 
			
		||||
		ext.Error.Set(span, true)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return resultVar0, resultVar1
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (a *OpenTracingAppLayer) GetCPAValue(valueID string) (*model.PropertyValue, *model.AppError) {
 | 
			
		||||
	origCtx := a.ctx
 | 
			
		||||
	span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetCPAValue")
 | 
			
		||||
 | 
			
		||||
	a.ctx = newCtx
 | 
			
		||||
	a.app.Srv().Store().SetContext(newCtx)
 | 
			
		||||
	defer func() {
 | 
			
		||||
		a.app.Srv().Store().SetContext(origCtx)
 | 
			
		||||
		a.ctx = origCtx
 | 
			
		||||
	}()
 | 
			
		||||
 | 
			
		||||
	defer span.Finish()
 | 
			
		||||
	resultVar0, resultVar1 := a.app.GetCPAValue(valueID)
 | 
			
		||||
 | 
			
		||||
	if resultVar1 != nil {
 | 
			
		||||
		span.LogFields(spanlog.Error(resultVar1))
 | 
			
		||||
		ext.Error.Set(span, true)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return resultVar0, resultVar1
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (a *OpenTracingAppLayer) GetChannel(c request.CTX, channelID string) (*model.Channel, *model.AppError) {
 | 
			
		||||
	origCtx := a.ctx
 | 
			
		||||
	span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetChannel")
 | 
			
		||||
@@ -12677,6 +12765,50 @@ func (a *OpenTracingAppLayer) ListAutocompleteCommands(teamID string, T i18n.Tra
 | 
			
		||||
	return resultVar0, resultVar1
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (a *OpenTracingAppLayer) ListCPAFields() ([]*model.PropertyField, *model.AppError) {
 | 
			
		||||
	origCtx := a.ctx
 | 
			
		||||
	span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.ListCPAFields")
 | 
			
		||||
 | 
			
		||||
	a.ctx = newCtx
 | 
			
		||||
	a.app.Srv().Store().SetContext(newCtx)
 | 
			
		||||
	defer func() {
 | 
			
		||||
		a.app.Srv().Store().SetContext(origCtx)
 | 
			
		||||
		a.ctx = origCtx
 | 
			
		||||
	}()
 | 
			
		||||
 | 
			
		||||
	defer span.Finish()
 | 
			
		||||
	resultVar0, resultVar1 := a.app.ListCPAFields()
 | 
			
		||||
 | 
			
		||||
	if resultVar1 != nil {
 | 
			
		||||
		span.LogFields(spanlog.Error(resultVar1))
 | 
			
		||||
		ext.Error.Set(span, true)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return resultVar0, resultVar1
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (a *OpenTracingAppLayer) ListCPAValues(userID string) ([]*model.PropertyValue, *model.AppError) {
 | 
			
		||||
	origCtx := a.ctx
 | 
			
		||||
	span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.ListCPAValues")
 | 
			
		||||
 | 
			
		||||
	a.ctx = newCtx
 | 
			
		||||
	a.app.Srv().Store().SetContext(newCtx)
 | 
			
		||||
	defer func() {
 | 
			
		||||
		a.app.Srv().Store().SetContext(origCtx)
 | 
			
		||||
		a.ctx = origCtx
 | 
			
		||||
	}()
 | 
			
		||||
 | 
			
		||||
	defer span.Finish()
 | 
			
		||||
	resultVar0, resultVar1 := a.app.ListCPAValues(userID)
 | 
			
		||||
 | 
			
		||||
	if resultVar1 != nil {
 | 
			
		||||
		span.LogFields(spanlog.Error(resultVar1))
 | 
			
		||||
		ext.Error.Set(span, true)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return resultVar0, resultVar1
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (a *OpenTracingAppLayer) ListDirectory(path string) ([]string, *model.AppError) {
 | 
			
		||||
	origCtx := a.ctx
 | 
			
		||||
	span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.ListDirectory")
 | 
			
		||||
@@ -13367,6 +13499,50 @@ func (a *OpenTracingAppLayer) PatchBot(rctx request.CTX, botUserId string, botPa
 | 
			
		||||
	return resultVar0, resultVar1
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (a *OpenTracingAppLayer) PatchCPAField(fieldID string, patch *model.PropertyFieldPatch) (*model.PropertyField, *model.AppError) {
 | 
			
		||||
	origCtx := a.ctx
 | 
			
		||||
	span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.PatchCPAField")
 | 
			
		||||
 | 
			
		||||
	a.ctx = newCtx
 | 
			
		||||
	a.app.Srv().Store().SetContext(newCtx)
 | 
			
		||||
	defer func() {
 | 
			
		||||
		a.app.Srv().Store().SetContext(origCtx)
 | 
			
		||||
		a.ctx = origCtx
 | 
			
		||||
	}()
 | 
			
		||||
 | 
			
		||||
	defer span.Finish()
 | 
			
		||||
	resultVar0, resultVar1 := a.app.PatchCPAField(fieldID, patch)
 | 
			
		||||
 | 
			
		||||
	if resultVar1 != nil {
 | 
			
		||||
		span.LogFields(spanlog.Error(resultVar1))
 | 
			
		||||
		ext.Error.Set(span, true)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return resultVar0, resultVar1
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (a *OpenTracingAppLayer) PatchCPAValue(userID string, fieldID string, value string) (*model.PropertyValue, *model.AppError) {
 | 
			
		||||
	origCtx := a.ctx
 | 
			
		||||
	span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.PatchCPAValue")
 | 
			
		||||
 | 
			
		||||
	a.ctx = newCtx
 | 
			
		||||
	a.app.Srv().Store().SetContext(newCtx)
 | 
			
		||||
	defer func() {
 | 
			
		||||
		a.app.Srv().Store().SetContext(origCtx)
 | 
			
		||||
		a.ctx = origCtx
 | 
			
		||||
	}()
 | 
			
		||||
 | 
			
		||||
	defer span.Finish()
 | 
			
		||||
	resultVar0, resultVar1 := a.app.PatchCPAValue(userID, fieldID, value)
 | 
			
		||||
 | 
			
		||||
	if resultVar1 != nil {
 | 
			
		||||
		span.LogFields(spanlog.Error(resultVar1))
 | 
			
		||||
		ext.Error.Set(span, true)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return resultVar0, resultVar1
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (a *OpenTracingAppLayer) PatchChannel(c request.CTX, channel *model.Channel, patch *model.ChannelPatch, userID string) (*model.Channel, *model.AppError) {
 | 
			
		||||
	origCtx := a.ctx
 | 
			
		||||
	span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.PatchChannel")
 | 
			
		||||
 
 | 
			
		||||
@@ -16,6 +16,10 @@ import (
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func (s *SqlPropertyFieldStore) propertyFieldToInsertMap(field *model.PropertyField) (map[string]any, error) {
 | 
			
		||||
	if field.Attrs == nil {
 | 
			
		||||
		field.Attrs = make(map[string]any)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	attrsJSON, err := json.Marshal(field.Attrs)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, errors.Wrap(err, "property_field_to_insert_map_marshal_attrs")
 | 
			
		||||
@@ -39,6 +43,10 @@ func (s *SqlPropertyFieldStore) propertyFieldToInsertMap(field *model.PropertyFi
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (s *SqlPropertyFieldStore) propertyFieldToUpdateMap(field *model.PropertyField) (map[string]any, error) {
 | 
			
		||||
	if field.Attrs == nil {
 | 
			
		||||
		field.Attrs = make(map[string]any)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	attrsJSON, err := json.Marshal(field.Attrs)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, errors.Wrap(err, "property_field_to_update_map_marshal_attrs")
 | 
			
		||||
 
 | 
			
		||||
@@ -685,6 +685,17 @@ func (c *Context) RequireRoleId() *Context {
 | 
			
		||||
	return c
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (c *Context) RequireFieldId() *Context {
 | 
			
		||||
	if c.Err != nil {
 | 
			
		||||
		return c
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if !model.IsValidId(c.Params.FieldId) {
 | 
			
		||||
		c.SetInvalidURLParam("field_id")
 | 
			
		||||
	}
 | 
			
		||||
	return c
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (c *Context) RequireSchemeId() *Context {
 | 
			
		||||
	if c.Err != nil {
 | 
			
		||||
		return c
 | 
			
		||||
 
 | 
			
		||||
@@ -111,6 +111,9 @@ type Params struct {
 | 
			
		||||
 | 
			
		||||
	// Cloud
 | 
			
		||||
	InvoiceId string
 | 
			
		||||
 | 
			
		||||
	// Custom Profile Attributes
 | 
			
		||||
	FieldId string
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func ParamsFromRequest(r *http.Request) *Params {
 | 
			
		||||
@@ -178,6 +181,7 @@ func ParamsFromRequest(r *http.Request) *Params {
 | 
			
		||||
	params.ExcludeHome, _ = strconv.ParseBool(query.Get("exclude_home"))
 | 
			
		||||
	params.ExcludeRemote, _ = strconv.ParseBool(query.Get("exclude_remote"))
 | 
			
		||||
	params.ChannelBookmarkId = props["bookmark_id"]
 | 
			
		||||
	params.FieldId = props["field_id"]
 | 
			
		||||
	params.Scope = query.Get("scope")
 | 
			
		||||
 | 
			
		||||
	if val, err := strconv.Atoi(query.Get("page")); err != nil || val < 0 {
 | 
			
		||||
 
 | 
			
		||||
@@ -4974,6 +4974,54 @@
 | 
			
		||||
    "id": "app.custom_group.unique_name",
 | 
			
		||||
    "translation": "group name is not unique"
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    "id": "app.custom_profile_attributes.cpa_group_id.app_error",
 | 
			
		||||
    "translation": "Cannot register Custom Profile Attributes property group"
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    "id": "app.custom_profile_attributes.create_property_field.app_error",
 | 
			
		||||
    "translation": "Unable to create Custom Profile Attribute field"
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    "id": "app.custom_profile_attributes.get_property_field.app_error",
 | 
			
		||||
    "translation": "Unable to get Custom Profile Attribute field"
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    "id": "app.custom_profile_attributes.limit_reached.app_error",
 | 
			
		||||
    "translation": "Custom Profile Attributes field limit reached"
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    "id": "app.custom_profile_attributes.list_property_values.app_error",
 | 
			
		||||
    "translation": "Unable to get custom profile attribute values"
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    "id": "app.custom_profile_attributes.property_field_delete.app_error",
 | 
			
		||||
    "translation": "Unable to delete Custom Profile Attribute field"
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    "id": "app.custom_profile_attributes.property_field_not_found.app_error",
 | 
			
		||||
    "translation": "Custom Profile Attribute field not found"
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    "id": "app.custom_profile_attributes.property_field_update.app_error",
 | 
			
		||||
    "translation": "Unable to update Custom Profile Attribute field"
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    "id": "app.custom_profile_attributes.property_value_creation.app_error",
 | 
			
		||||
    "translation": "Cannot create property value"
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    "id": "app.custom_profile_attributes.property_value_list.app_error",
 | 
			
		||||
    "translation": "Unable to retrieve property values"
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    "id": "app.custom_profile_attributes.property_value_update.app_error",
 | 
			
		||||
    "translation": "Cannot update property value"
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    "id": "app.custom_profile_attributes.search_property_fields.app_error",
 | 
			
		||||
    "translation": "Unable to search Custom Profile Attribute fields"
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    "id": "app.delete_scheduled_post.delete_error",
 | 
			
		||||
    "translation": "Failed to delete scheduled post from database."
 | 
			
		||||
 
 | 
			
		||||
@@ -600,6 +600,26 @@ func (c *Client4) limitsRoute() string {
 | 
			
		||||
	return "/limits"
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (c *Client4) customProfileAttributesRoute() string {
 | 
			
		||||
	return "/custom_profile_attributes"
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (c *Client4) userCustomProfileAttributesRoute(userID string) string {
 | 
			
		||||
	return fmt.Sprintf("%s/%s", c.userRoute(userID), c.customProfileAttributesRoute())
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (c *Client4) customProfileAttributeFieldsRoute() string {
 | 
			
		||||
	return fmt.Sprintf("%s/fields", c.customProfileAttributesRoute())
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (c *Client4) customProfileAttributeFieldRoute(fieldID string) string {
 | 
			
		||||
	return fmt.Sprintf("%s/%s", c.customProfileAttributeFieldsRoute(), fieldID)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (c *Client4) customProfileAttributeValuesRoute() string {
 | 
			
		||||
	return fmt.Sprintf("%s/values", c.customProfileAttributesRoute())
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (c *Client4) GetServerLimits(ctx context.Context) (*ServerLimits, *Response, error) {
 | 
			
		||||
	r, err := c.DoAPIGet(ctx, c.limitsRoute()+"/users", "")
 | 
			
		||||
	if err != nil {
 | 
			
		||||
@@ -9383,3 +9403,96 @@ func (c *Client4) RestorePostVersion(ctx context.Context, postId, versionId stri
 | 
			
		||||
	}
 | 
			
		||||
	return restoredPost, BuildResponse(r), nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (c *Client4) CreateCPAField(ctx context.Context, field *PropertyField) (*PropertyField, *Response, error) {
 | 
			
		||||
	buf, err := json.Marshal(field)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, nil, NewAppError("CreateCPAField", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
 | 
			
		||||
	}
 | 
			
		||||
	r, err := c.DoAPIPostBytes(ctx, c.customProfileAttributeFieldsRoute(), buf)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, BuildResponse(r), err
 | 
			
		||||
	}
 | 
			
		||||
	defer closeBody(r)
 | 
			
		||||
 | 
			
		||||
	var pf PropertyField
 | 
			
		||||
	if err := json.NewDecoder(r.Body).Decode(&pf); err != nil {
 | 
			
		||||
		return nil, nil, NewAppError("CreateCPAField", "api.unmarshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
 | 
			
		||||
	}
 | 
			
		||||
	return &pf, BuildResponse(r), nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (c *Client4) ListCPAFields(ctx context.Context) ([]*PropertyField, *Response, error) {
 | 
			
		||||
	r, err := c.DoAPIGet(ctx, c.customProfileAttributeFieldsRoute(), "")
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, BuildResponse(r), err
 | 
			
		||||
	}
 | 
			
		||||
	defer closeBody(r)
 | 
			
		||||
 | 
			
		||||
	var fields []*PropertyField
 | 
			
		||||
	if err := json.NewDecoder(r.Body).Decode(&fields); err != nil {
 | 
			
		||||
		return nil, nil, NewAppError("ListCPAFields", "api.unmarshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
 | 
			
		||||
	}
 | 
			
		||||
	return fields, BuildResponse(r), nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (c *Client4) PatchCPAField(ctx context.Context, fieldID string, patch *PropertyFieldPatch) (*PropertyField, *Response, error) {
 | 
			
		||||
	buf, err := json.Marshal(patch)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, nil, NewAppError("PatchCPAField", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
 | 
			
		||||
	}
 | 
			
		||||
	r, err := c.DoAPIPatchBytes(ctx, c.customProfileAttributeFieldRoute(fieldID), buf)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, BuildResponse(r), err
 | 
			
		||||
	}
 | 
			
		||||
	defer closeBody(r)
 | 
			
		||||
 | 
			
		||||
	var pf PropertyField
 | 
			
		||||
	if err := json.NewDecoder(r.Body).Decode(&pf); err != nil {
 | 
			
		||||
		return nil, nil, NewAppError("PatchCPAField", "api.unmarshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
 | 
			
		||||
	}
 | 
			
		||||
	return &pf, BuildResponse(r), nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (c *Client4) DeleteCPAField(ctx context.Context, fieldID string) (*Response, error) {
 | 
			
		||||
	r, err := c.DoAPIDelete(ctx, c.customProfileAttributeFieldRoute(fieldID))
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return BuildResponse(r), err
 | 
			
		||||
	}
 | 
			
		||||
	defer closeBody(r)
 | 
			
		||||
	return BuildResponse(r), nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (c *Client4) ListCPAValues(ctx context.Context, userID string) (map[string]string, *Response, error) {
 | 
			
		||||
	r, err := c.DoAPIGet(ctx, c.userCustomProfileAttributesRoute(userID), "")
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, BuildResponse(r), err
 | 
			
		||||
	}
 | 
			
		||||
	defer closeBody(r)
 | 
			
		||||
 | 
			
		||||
	fields := make(map[string]string)
 | 
			
		||||
	if err := json.NewDecoder(r.Body).Decode(&fields); err != nil {
 | 
			
		||||
		return nil, nil, NewAppError("ListCPAValues", "api.unmarshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
 | 
			
		||||
	}
 | 
			
		||||
	return fields, BuildResponse(r), nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (c *Client4) PatchCPAValues(ctx context.Context, values map[string]string) (map[string]string, *Response, error) {
 | 
			
		||||
	buf, err := json.Marshal(values)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, nil, NewAppError("PatchCPAValues", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	r, err := c.DoAPIPatchBytes(ctx, c.customProfileAttributeValuesRoute(), buf)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, BuildResponse(r), err
 | 
			
		||||
	}
 | 
			
		||||
	defer closeBody(r)
 | 
			
		||||
 | 
			
		||||
	var patchedValues map[string]string
 | 
			
		||||
	if err := json.NewDecoder(r.Body).Decode(&patchedValues); err != nil {
 | 
			
		||||
		return nil, nil, NewAppError("PatchCPAValues", "api.unmarshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return patchedValues, BuildResponse(r), nil
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										6
									
								
								server/public/model/custom_profile_attributes.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								server/public/model/custom_profile_attributes.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,6 @@
 | 
			
		||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
 | 
			
		||||
// See LICENSE.txt for license information.
 | 
			
		||||
 | 
			
		||||
package model
 | 
			
		||||
 | 
			
		||||
const CustomProfileAttributesPropertyGroupName = "custom_profile_attributes"
 | 
			
		||||
@@ -57,6 +57,8 @@ type FeatureFlags struct {
 | 
			
		||||
	ExperimentalAuditSettingsSystemConsoleUI bool
 | 
			
		||||
 | 
			
		||||
	ExperimentalCrossTeamSearch bool
 | 
			
		||||
 | 
			
		||||
	CustomProfileAttributes bool
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (f *FeatureFlags) SetDefaults() {
 | 
			
		||||
@@ -81,6 +83,7 @@ func (f *FeatureFlags) SetDefaults() {
 | 
			
		||||
	f.NotificationMonitoring = true
 | 
			
		||||
	f.ExperimentalAuditSettingsSystemConsoleUI = false
 | 
			
		||||
	f.ExperimentalCrossTeamSearch = false
 | 
			
		||||
	f.CustomProfileAttributes = false
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// ToMap returns the feature flags as a map[string]string
 | 
			
		||||
 
 | 
			
		||||
@@ -3,7 +3,10 @@
 | 
			
		||||
 | 
			
		||||
package model
 | 
			
		||||
 | 
			
		||||
import "net/http"
 | 
			
		||||
import (
 | 
			
		||||
	"net/http"
 | 
			
		||||
	"strings"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type PropertyFieldType string
 | 
			
		||||
 | 
			
		||||
@@ -29,6 +32,21 @@ type PropertyField struct {
 | 
			
		||||
	DeleteAt   int64             `json:"delete_at"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (pf *PropertyField) Auditable() map[string]interface{} {
 | 
			
		||||
	return map[string]interface{}{
 | 
			
		||||
		"id":          pf.ID,
 | 
			
		||||
		"group_id":    pf.GroupID,
 | 
			
		||||
		"name":        pf.Name,
 | 
			
		||||
		"type":        pf.Type,
 | 
			
		||||
		"attrs":       pf.Attrs,
 | 
			
		||||
		"target_id":   pf.TargetID,
 | 
			
		||||
		"target_type": pf.TargetType,
 | 
			
		||||
		"create_at":   pf.CreateAt,
 | 
			
		||||
		"update_at":   pf.UpdateAt,
 | 
			
		||||
		"delete_at":   pf.DeleteAt,
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (pf *PropertyField) PreSave() {
 | 
			
		||||
	if pf.ID == "" {
 | 
			
		||||
		pf.ID = NewId()
 | 
			
		||||
@@ -73,6 +91,56 @@ func (pf *PropertyField) IsValid() error {
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (pf *PropertyField) SanitizeInput() {
 | 
			
		||||
	pf.Name = strings.TrimSpace(pf.Name)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type PropertyFieldPatch struct {
 | 
			
		||||
	Name       *string            `json:"name"`
 | 
			
		||||
	Type       *PropertyFieldType `json:"type"`
 | 
			
		||||
	Attrs      *map[string]any    `json:"attrs"`
 | 
			
		||||
	TargetID   *string            `json:"target_id"`
 | 
			
		||||
	TargetType *string            `json:"target_type"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (pfp *PropertyFieldPatch) Auditable() map[string]interface{} {
 | 
			
		||||
	return map[string]interface{}{
 | 
			
		||||
		"name":        pfp.Name,
 | 
			
		||||
		"type":        pfp.Type,
 | 
			
		||||
		"attrs":       pfp.Attrs,
 | 
			
		||||
		"target_id":   pfp.TargetID,
 | 
			
		||||
		"target_type": pfp.TargetType,
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (pfp *PropertyFieldPatch) SanitizeInput() {
 | 
			
		||||
	if pfp.Name != nil {
 | 
			
		||||
		pfp.Name = NewPointer(strings.TrimSpace(*pfp.Name))
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (pf *PropertyField) Patch(patch *PropertyFieldPatch) {
 | 
			
		||||
	if patch.Name != nil {
 | 
			
		||||
		pf.Name = *patch.Name
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if patch.Type != nil {
 | 
			
		||||
		pf.Type = *patch.Type
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if patch.Attrs != nil {
 | 
			
		||||
		pf.Attrs = *patch.Attrs
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if patch.TargetID != nil {
 | 
			
		||||
		pf.TargetID = *patch.TargetID
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if patch.TargetType != nil {
 | 
			
		||||
		pf.TargetType = *patch.TargetType
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type PropertyFieldSearchOpts struct {
 | 
			
		||||
	GroupID        string
 | 
			
		||||
	TargetType     string
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user