Merge branch 'user-claims' into 'master'

Add external device-user claim APIs and UI

Closes product-iots#346

See merge request entgra/carbon-device-mgt!478
4.x.x
Dharmakeerthi Lasantha 5 years ago
commit 03345edca3

@ -40,7 +40,7 @@ import Policies from './scenes/Home/scenes/Policies';
import AddNewPolicy from './scenes/Home/scenes/Policies/scenes/AddNewPolicy'; import AddNewPolicy from './scenes/Home/scenes/Policies/scenes/AddNewPolicy';
import Roles from './scenes/Home/scenes/Roles'; import Roles from './scenes/Home/scenes/Roles';
import DeviceTypes from './scenes/Home/scenes/DeviceTypes'; import DeviceTypes from './scenes/Home/scenes/DeviceTypes';
import Certificates from './scenes/Home/scenes/Certificates'; import Certificates from './scenes/Home/scenes/Configurations/scenes/Certificates';
import Devices from './scenes/Home/scenes/Devices'; import Devices from './scenes/Home/scenes/Devices';
const routes = [ const routes = [
@ -105,7 +105,7 @@ const routes = [
exact: true, exact: true,
}, },
{ {
path: '/entgra/certificates', path: '/entgra/configurations/certificates',
component: Certificates, component: Certificates,
exact: true, exact: true,
}, },

@ -151,7 +151,7 @@ class Home extends React.Component {
} }
> >
<Menu.Item key="certificates"> <Menu.Item key="certificates">
<Link to="/entgra/certificates"> <Link to="/entgra/configurations/certificates">
<span>Certificates</span> <span>Certificates</span>
</Link> </Link>
</Menu.Item> </Menu.Item>

@ -30,7 +30,7 @@ import {
import TimeAgo from 'javascript-time-ago'; import TimeAgo from 'javascript-time-ago';
// Load locale-specific relative date/time formatting rules. // Load locale-specific relative date/time formatting rules.
import en from 'javascript-time-ago/locale/en'; import en from 'javascript-time-ago/locale/en';
import { withConfigContext } from '../../../../../../components/ConfigContext'; import { withConfigContext } from '../../../../../../../../components/ConfigContext';
import Moment from 'react-moment'; import Moment from 'react-moment';
const { Paragraph, Text } = Typography; const { Paragraph, Text } = Typography;

@ -0,0 +1,221 @@
/*
* Copyright (c) 2020, Entgra (pvt) Ltd. (http://entgra.io) All Rights Reserved.
*
* Entgra (pvt) Ltd. licenses this file to you under the Apache License,
* Version 2.0 (the "License"); you may not use this file except
* in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import React from 'react';
import { Button, Form, Input, Modal, notification, Col, Row } from 'antd';
import axios from 'axios';
import { withConfigContext } from '../../../../../../../../components/ConfigContext';
import { handleApiError } from '../../../../../../../../services/utils/errorHandler';
const InputGroup = Input.Group;
class ExternalDevicesModal extends React.Component {
constructor(props) {
super(props);
this.config = this.props.context;
this.state = {
isDeviceEditModalVisible: false,
metaData: [],
};
}
openDeviceEditModal = () => {
this.setState({
isDeviceEditModalVisible: true,
});
this.getExternalDevicesForUser(this.props.user);
};
onCancelHandler = () => {
this.setState({
isDeviceEditModalVisible: false,
});
};
getExternalDevicesForUser = userName => {
let apiURL =
window.location.origin +
this.config.serverConfig.invoker.uri +
this.config.serverConfig.invoker.deviceMgt +
`/users/claims/${userName}`;
axios
.get(apiURL)
.then(res => {
if (res.status === 200) {
if (res.data.data.hasOwnProperty('http://wso2.org/claims/devices')) {
this.setState({
metaData: JSON.parse(
res.data.data['http://wso2.org/claims/devices'],
),
});
}
}
})
.catch(error => {
handleApiError(
error,
'Error occurred while trying to retrieve claims.',
);
});
};
setExternalDevicesForUser = (userName, payload) => {
let apiURL =
window.location.origin +
this.config.serverConfig.invoker.uri +
this.config.serverConfig.invoker.deviceMgt +
`/users/claims/${userName}`;
axios
.put(apiURL, payload)
.then(res => {
if (res.status === 200) {
notification.success({
message: 'Done',
duration: 0,
description: 'Succussfully updated.',
});
}
this.setState({
isDeviceEditModalVisible: false,
});
})
.catch(error => {
handleApiError(error, 'Error occurred while trying to update claims.');
});
};
onSubmitClaims = e => {
this.props.form.validateFields(['meta'], (err, values) => {
if (!err) {
this.setExternalDevicesForUser(this.props.user, this.state.metaData);
}
});
};
addNewMetaData = () => {
this.setState({
metaData: [...this.state.metaData, { deviceName: '', id: '' }],
});
};
render() {
const { getFieldDecorator } = this.props.form;
const { metaData } = this.state;
return (
<div>
<div>
<Button
type="primary"
size={'small'}
icon="desktop"
onClick={this.openDeviceEditModal}
>
External Devices
</Button>
</div>
<div>
<Modal
title="EDIT EXTERNAL DEVICE CLAIMS"
width="40%"
visible={this.state.isDeviceEditModalVisible}
onOk={this.onSubmitClaims}
onCancel={this.onCancelHandler}
footer={[
<Button key="cancel" onClick={this.onCancelHandler}>
Cancel
</Button>,
<Button key="submit" type="primary" onClick={this.onSubmitClaims}>
Update
</Button>,
]}
>
<div style={{ alignItems: 'center' }}>
<p>Add or edit external device information</p>
<Form labelCol={{ span: 5 }} wrapperCol={{ span: 18 }}>
<Form.Item style={{ display: 'block' }}>
{getFieldDecorator('meta', {})(
<div>
{metaData.map((data, index) => {
return (
<InputGroup key={index}>
<Row gutter={8}>
<Col span={5}>
<Input
placeholder="key"
defaultValue={data.deviceName}
onChange={e => {
metaData[index].deviceName =
e.currentTarget.value;
this.setState({
metaData,
});
}}
/>
</Col>
<Col span={8}>
<Input
placeholder="value"
defaultValue={data.id}
onChange={e => {
metaData[index].id = e.currentTarget.value;
this.setState({
metaData,
});
}}
/>
</Col>
<Col span={3}>
<Button
type="dashed"
shape="circle"
icon="minus"
onClick={() => {
metaData.splice(index, 1);
this.setState({
metaData,
});
}}
/>
</Col>
</Row>
</InputGroup>
);
})}
<Button
type="dashed"
icon="plus"
onClick={this.addNewMetaData}
>
Addd
</Button>
</div>,
)}
</Form.Item>
</Form>
</div>
</Modal>
</div>
</div>
);
}
}
export default withConfigContext(
Form.create({ name: 'external-device-modal' })(ExternalDevicesModal),
);

@ -27,6 +27,7 @@ import UsersDevices from './components/UserDevices';
import AddUser from './components/AddUser'; import AddUser from './components/AddUser';
import UserActions from './components/UserActions'; import UserActions from './components/UserActions';
import Filter from '../../../../components/Filter'; import Filter from '../../../../components/Filter';
import ExternalDevicesModal from './components/ExternalDevicesModal';
const ButtonGroup = Button.Group; const ButtonGroup = Button.Group;
let apiUrl; let apiUrl;
@ -114,8 +115,6 @@ class UsersTable extends React.Component {
data: res.data.data.users, data: res.data.data.users,
pagination, pagination,
}); });
console.log(res.data.data);
} }
}) })
.catch(error => { .catch(error => {
@ -224,6 +223,7 @@ class UsersTable extends React.Component {
title: 'First Name', title: 'First Name',
dataIndex: 'firstname', dataIndex: 'firstname',
key: 'firstname', key: 'firstname',
width: 200,
}, },
{ {
title: 'Last Name', title: 'Last Name',
@ -260,6 +260,12 @@ class UsersTable extends React.Component {
</ButtonGroup> </ButtonGroup>
), ),
}, },
{
title: 'External Device Claims',
dataIndex: 'claims',
key: 'claims',
render: (id, row) => <ExternalDevicesModal user={row.username} />,
},
{ {
title: 'Action', title: 'Action',
dataIndex: 'id', dataIndex: 'id',

@ -34,6 +34,8 @@
*/ */
package org.wso2.carbon.device.mgt.jaxrs.service.api; package org.wso2.carbon.device.mgt.jaxrs.service.api;
import com.google.gson.JsonArray;
import com.google.gson.JsonObject;
import io.swagger.annotations.SwaggerDefinition; import io.swagger.annotations.SwaggerDefinition;
import io.swagger.annotations.Info; import io.swagger.annotations.Info;
import io.swagger.annotations.ExtensionProperty; import io.swagger.annotations.ExtensionProperty;
@ -1009,4 +1011,161 @@ public interface UserManagementService {
"Provide the value in the following format: EEE, d MMM yyyy HH:mm:ss Z\n." + "Provide the value in the following format: EEE, d MMM yyyy HH:mm:ss Z\n." +
"Example: Mon, 05 Jan 2014 15:10:00 +0200") "Example: Mon, 05 Jan 2014 15:10:00 +0200")
@HeaderParam("If-Modified-Since") String ifModifiedSince); @HeaderParam("If-Modified-Since") String ifModifiedSince);
@PUT
@Path("/claims/{username}")
@ApiOperation(
produces = MediaType.APPLICATION_JSON,
httpMethod = "PUT",
value = "Updating external device claims of user",
notes = "Update external device claims of a user registered with Entgra IoTS using the REST API.",
response = BasicUserInfo.class,
tags = "User Management",
extensions = {
@Extension(properties = {
@ExtensionProperty(name = Constants.SCOPE, value = "perm:users:details")
})
}
)
@ApiResponses(value = {
@ApiResponse(
code = 200,
message = "OK. \n Successfully updated external device claims of user.",
response = BasicUserInfo.class,
responseHeaders = {
@ResponseHeader(
name = "Content-Type",
description = "The content type of the body"),
@ResponseHeader(
name = "ETag",
description = "Entity Tag of the response resource.\n" +
"Used by caches, or in conditional requests."),
@ResponseHeader(
name = "Last-Modified",
description = "Date and time the resource was last modified.\n" +
"Used by caches, or in conditional requests."),
}),
@ApiResponse(
code = 404,
message = "Not Found. \n The specified resource does not exist.",
response = ErrorResponse.class),
@ApiResponse(
code = 500,
message = "Internal Server ErrorResponse. \n Server error occurred while" +
" fetching the user details.",
response = ErrorResponse.class)
})
Response updateUserClaimsForDevices(
@ApiParam(
name = "username",
value = "Provide the username of the user.",
required = true,
defaultValue = "admin")
@PathParam("username") String username,
@ApiParam(
name = "device list",
value = "Array of objects with device details",
required = true) JsonArray deviceList);
@GET
@Path("/claims/{username}")
@ApiOperation(
produces = MediaType.APPLICATION_JSON,
httpMethod = "GET",
value = "Getting external device claims of user",
notes = "Get external device claims of a user registered with Entgra IoTS using the REST API.",
response = BasicUserInfo.class,
tags = "User Management",
extensions = {
@Extension(properties = {
@ExtensionProperty(name = Constants.SCOPE, value = "perm:users:details")
})
}
)
@ApiResponses(value = {
@ApiResponse(
code = 200,
message = "OK. \n Successfully fetched external device claims of user.",
response = BasicUserInfo.class,
responseHeaders = {
@ResponseHeader(
name = "Content-Type",
description = "The content type of the body"),
@ResponseHeader(
name = "ETag",
description = "Entity Tag of the response resource.\n" +
"Used by caches, or in conditional requests."),
@ResponseHeader(
name = "Last-Modified",
description = "Date and time the resource was last modified.\n" +
"Used by caches, or in conditional requests."),
}),
@ApiResponse(
code = 404,
message = "Not Found. \n The specified resource does not exist.",
response = ErrorResponse.class),
@ApiResponse(
code = 500,
message = "Internal Server ErrorResponse. \n Server error occurred while" +
" fetching the ruser details.",
response = ErrorResponse.class)
})
Response getUserClaimsForDevices(
@ApiParam(
name = "username",
value = "Provide the username of the user.",
required = true,
defaultValue = "admin")
@PathParam("username") String username);
@DELETE
@Path("/claims/{username}")
@ApiOperation(
produces = MediaType.APPLICATION_JSON,
httpMethod = "DELETE",
value = "Deleting external device claims of user",
notes = "Delete external device claims of user registered with Entgra IoTS using the REST API.",
response = BasicUserInfo.class,
tags = "User Management",
extensions = {
@Extension(properties = {
@ExtensionProperty(name = Constants.SCOPE, value = "perm:users:details")
})
}
)
@ApiResponses(value = {
@ApiResponse(
code = 200,
message = "OK. \n Successfully deleted external device claims of user.",
response = BasicUserInfo.class,
responseHeaders = {
@ResponseHeader(
name = "Content-Type",
description = "The content type of the body"),
@ResponseHeader(
name = "ETag",
description = "Entity Tag of the response resource.\n" +
"Used by caches, or in conditional requests."),
@ResponseHeader(
name = "Last-Modified",
description = "Date and time the resource was last modified.\n" +
"Used by caches, or in conditional requests."),
}),
@ApiResponse(
code = 404,
message = "Not Found. \n The specified resource does not exist.",
response = ErrorResponse.class),
@ApiResponse(
code = 500,
message = "Internal Server ErrorResponse. \n Server error occurred while" +
" fetching the ruser details.",
response = ErrorResponse.class)
})
Response deleteUserClaimsForDevices(
@ApiParam(
name = "username",
value = "Provide the username of the user.",
required = true,
defaultValue = "admin")
@PathParam("username") String username);
} }

@ -33,6 +33,7 @@
*/ */
package org.wso2.carbon.device.mgt.jaxrs.service.impl; package org.wso2.carbon.device.mgt.jaxrs.service.impl;
import com.google.gson.JsonArray;
import com.google.gson.JsonObject; import com.google.gson.JsonObject;
import org.apache.commons.lang.StringUtils; import org.apache.commons.lang.StringUtils;
import org.apache.commons.logging.Log; import org.apache.commons.logging.Log;
@ -71,6 +72,7 @@ import org.wso2.carbon.user.api.RealmConfiguration;
import org.wso2.carbon.user.api.UserRealm; import org.wso2.carbon.user.api.UserRealm;
import org.wso2.carbon.user.api.UserStoreException; import org.wso2.carbon.user.api.UserStoreException;
import org.wso2.carbon.user.api.UserStoreManager; import org.wso2.carbon.user.api.UserStoreManager;
import org.wso2.carbon.user.core.UserCoreConstants;
import org.wso2.carbon.user.core.service.RealmService; import org.wso2.carbon.user.core.service.RealmService;
import org.wso2.carbon.utils.CarbonUtils; import org.wso2.carbon.utils.CarbonUtils;
import org.wso2.carbon.utils.multitenancy.MultitenantConstants; import org.wso2.carbon.utils.multitenancy.MultitenantConstants;
@ -920,6 +922,113 @@ public class UserManagementServiceImpl implements UserManagementService {
} }
} }
@PUT
@Override
@Path("/claims/{username}")
public Response updateUserClaimsForDevices(
@PathParam("username") String username,
JsonArray deviceList) {
try {
RealmConfiguration realmConfiguration = PrivilegedCarbonContext.getThreadLocalCarbonContext()
.getUserRealm()
.getRealmConfiguration();
String domain = realmConfiguration
.getUserStoreProperty(UserCoreConstants.RealmConfig.PROPERTY_DOMAIN_NAME);
if (!StringUtils.isBlank(domain)) {
username = domain + Constants.FORWARD_SLASH + username;
}
UserStoreManager userStoreManager = DeviceMgtAPIUtils.getUserStoreManager();
if (!userStoreManager.isExistingUser(username)) {
if (log.isDebugEnabled()) {
log.debug("User by username: " + username + " does not exist.");
}
return Response.status(Response.Status.NOT_FOUND).entity(
new ErrorResponse.ErrorResponseBuilder().setMessage(
"User doesn't exist.").build()).build();
}
Map<String, String> userClaims =
this.buildExternalDevicesUserClaims(username, domain, deviceList, userStoreManager);
userStoreManager.setUserClaimValues(username, userClaims, domain);
return Response.status(Response.Status.OK).entity(userClaims).build();
} catch (UserStoreException e) {
String msg = "Error occurred while updating external device claims of the user '" + username + "'";
log.error(msg, e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR).entity(
new ErrorResponse.ErrorResponseBuilder().setMessage(msg).build()).build();
}
}
@GET
@Override
@Path("/claims/{username}")
public Response getUserClaimsForDevices(
@PathParam("username") String username) {
try {
RealmConfiguration realmConfiguration = PrivilegedCarbonContext.getThreadLocalCarbonContext()
.getUserRealm()
.getRealmConfiguration();
String domain = realmConfiguration
.getUserStoreProperty(UserCoreConstants.RealmConfig.PROPERTY_DOMAIN_NAME);
if (!StringUtils.isBlank(domain)) {
username = domain + Constants.FORWARD_SLASH + username;
}
UserStoreManager userStoreManager = DeviceMgtAPIUtils.getUserStoreManager();
if (!userStoreManager.isExistingUser(username)) {
if (log.isDebugEnabled()) {
log.debug("User by username: " + username + " does not exist.");
}
return Response.status(Response.Status.NOT_FOUND).entity(
new ErrorResponse.ErrorResponseBuilder().setMessage(
"User doesn't exist.").build()).build();
}
String[] claimArray = {Constants.USER_CLAIM_DEVICES};
Map<String, String> claims = userStoreManager.getUserClaimValues(username, claimArray, domain);
return Response.status(Response.Status.OK).entity(claims).build();
} catch (UserStoreException e) {
String msg = "Error occurred while retrieving external device claims of the user '" + username + "'";
log.error(msg, e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR).entity(
new ErrorResponse.ErrorResponseBuilder().setMessage(msg).build()).build();
}
}
@DELETE
@Override
@Path("/claims/{username}")
public Response deleteUserClaimsForDevices(
@PathParam("username") String username) {
try {
RealmConfiguration realmConfiguration = PrivilegedCarbonContext.getThreadLocalCarbonContext()
.getUserRealm()
.getRealmConfiguration();
String domain = realmConfiguration
.getUserStoreProperty(UserCoreConstants.RealmConfig.PROPERTY_DOMAIN_NAME);
if (!StringUtils.isBlank(domain)) {
username = domain + Constants.FORWARD_SLASH + username;
}
UserStoreManager userStoreManager = DeviceMgtAPIUtils.getUserStoreManager();
if (!userStoreManager.isExistingUser(username)) {
if (log.isDebugEnabled()) {
log.debug("User by username: " + username + " does not exist.");
}
return Response.status(Response.Status.NOT_FOUND).entity(
new ErrorResponse.ErrorResponseBuilder().setMessage(
"User doesn't exist.").build()).build();
}
String[] claimArray = {Constants.USER_CLAIM_DEVICES};
userStoreManager.deleteUserClaimValues(
username,
claimArray,
domain);
return Response.status(Response.Status.OK).entity(claimArray).build();
} catch (UserStoreException e) {
String msg = "Error occurred while deleting external device claims of the user '" + username + "'";
log.error(msg, e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR).entity(
new ErrorResponse.ErrorResponseBuilder().setMessage(msg).build()).build();
}
}
private Map<String, String> buildDefaultUserClaims(String firstName, String lastName, String emailAddress, private Map<String, String> buildDefaultUserClaims(String firstName, String lastName, String emailAddress,
boolean isFresh) { boolean isFresh) {
Map<String, String> defaultUserClaims = new HashMap<>(); Map<String, String> defaultUserClaims = new HashMap<>();
@ -937,6 +1046,40 @@ public class UserManagementServiceImpl implements UserManagementService {
return defaultUserClaims; return defaultUserClaims;
} }
/**
* This method is used to build String map for user claims with updated external device details
*
* @param username username of the particular user
* @param domain domain of the particular user
* @param deviceList Array of external device details
* @param userStoreManager {@link UserStoreManager} instance
* @return String map
* @throws UserStoreException If any error occurs while calling into UserStoreManager service
*/
private Map<String, String> buildExternalDevicesUserClaims(
String username,
String domain,
JsonArray deviceList,
UserStoreManager userStoreManager) throws UserStoreException {
Map<String, String> userClaims;
String[] claimArray = {
Constants.USER_CLAIM_FIRST_NAME,
Constants.USER_CLAIM_LAST_NAME,
Constants.USER_CLAIM_EMAIL_ADDRESS,
Constants.USER_CLAIM_MODIFIED
};
userClaims = userStoreManager.getUserClaimValues(username, claimArray, domain);
if (userClaims.containsKey(Constants.USER_CLAIM_DEVICES)) {
userClaims.replace(Constants.USER_CLAIM_DEVICES, deviceList.toString());
} else {
userClaims.put(Constants.USER_CLAIM_DEVICES, deviceList.toString());
}
if (log.isDebugEnabled()) {
log.debug("Claim map is created for user: " + username + ", claims:" + userClaims.toString());
}
return userClaims;
}
private String generateInitialUserPassword() { private String generateInitialUserPassword() {
int passwordLength = 6; int passwordLength = 6;
//defining the pool of characters to be used for initial password generation //defining the pool of characters to be used for initial password generation

@ -45,12 +45,14 @@ public class Constants {
public static final String USER_CLAIM_LAST_NAME = "http://wso2.org/claims/lastname"; public static final String USER_CLAIM_LAST_NAME = "http://wso2.org/claims/lastname";
public static final String USER_CLAIM_CREATED = "http://wso2.org/claims/created"; public static final String USER_CLAIM_CREATED = "http://wso2.org/claims/created";
public static final String USER_CLAIM_MODIFIED = "http://wso2.org/claims/modified"; public static final String USER_CLAIM_MODIFIED = "http://wso2.org/claims/modified";
public static final String USER_CLAIM_DEVICES = "http://wso2.org/claims/devices";
public static final String PRIMARY_USER_STORE = "PRIMARY"; public static final String PRIMARY_USER_STORE = "PRIMARY";
public static final String DEFAULT_STREAM_VERSION = "1.0.0"; public static final String DEFAULT_STREAM_VERSION = "1.0.0";
public static final String SCOPE = "scope"; public static final String SCOPE = "scope";
public static final String JDBC_USERSTOREMANAGER = "org.wso2.carbon.user.core.jdbc.JDBCUserStoreManager"; public static final String JDBC_USERSTOREMANAGER = "org.wso2.carbon.user.core.jdbc.JDBCUserStoreManager";
public static final String DEFAULT_SIMPLE_DATE_FORMAT = "EEE, d MMM yyyy HH:mm:ss Z"; public static final String DEFAULT_SIMPLE_DATE_FORMAT = "EEE, d MMM yyyy HH:mm:ss Z";
public static final int DEFAULT_PAGE_LIMIT = 50; public static final int DEFAULT_PAGE_LIMIT = 50;
public static final String FORWARD_SLASH = "/";
public final class ErrorMessages { public final class ErrorMessages {

Loading…
Cancel
Save