I'm currently developing a back end and having my first run in with security laws etc. and it has complicated the design of my DB slightly:
Specification
Central server for app with DB containing limited user information (user_id, email, password (hashed and salted)) can be anywhere.
Organisations making use of our service require that all other information be stored in-house, so the database for that particular organisation is in their building.
The user IDs in our central database are used by multiple types of users in these organisations databases, where more info about that user is stored (phone number, name, address...)
Problem
With Spring Boot, I need to make it so the datasource used is determined by which user makes the request. I map users to their corresponding organisation's database within the central server so the information is there, but I'm not sure how to make this variable.
I understand there are methods involving adding another database config in the application.properties file. But as far as I'm aware this can't be changed (easily) once the server is deployed and running without a full redeploy, and I'm hoping to build this in such a way that adding another organisation only involves setting up their db, and adding another database details to the central server.
Extra detail
I'd like to use CrudRepository with hibernate entities for this. I plan on only generating user IDs on the central server.
Any pointers would be awesome.
Thanks!
The terminology for this is database multi-tenancy. There are multiple strategies for multi-tenancy: different databases, different schemas in the same database, and the same schema on one database with a defined discriminator.
You basically create a DataSourceBasedMultiTenantConnectionProviderImpl class which provides the connection to a datasource based on which tenant is requesting it, and a CurrentTenantIdentifierResolverImpl class which identifies who is the requesting tenant.
You can read more about it here. Since your tenants each have their own database, you would probably want to focus on the multi-tenancy separate database approach. It worked fine with CrudRepository when I implemented it. You also might want to find your own way of creating the tenant map, since I had 2 tenants and no need to add more at any point.
Heres a sample of the connection provider from when I implemented this:
public class DataSourceBasedMultiTenantConnectionProviderImpl extends AbstractDataSourceBasedMultiTenantConnectionProviderImpl {
private static final String DEFAULT_TENANT_ID = "A";
#Autowired
private DataSource datasourceA;
#Autowired
private DataSource datasourceB;
private Map<String, DataSource> map;
#PostConstruct
public void load() {
map = new HashMap<>();
map.put("A", datasourceA);
map.put("B", datasourceB);
}
#Override
protected DataSource selectAnyDataSource() {
return map.get(DEFAULT_TENANT_ID);
}
#Override
protected DataSource selectDataSource(String tenantIdentifier) {
return map.get(tenantIdentifier);
}
}
Related
I know this question is asked already many times, but I did not find a good answer for my case. I'm using SQLC to generate methods for querying the db. Everything is working fine when using one connection initialised at start. Now I need to set it up in a multi-tenant environment where each tenant will have a separate DB. For now I would like to start with a connection map (map[string]*sql.DB) connecting the tenant with a database connection. My question is about overriding/selecting the connection at runtime. with one connection the repository is initialised like:
type Repository interface {
GetCustomerById(ctx context.Context, id int64) (Customer, error)
ListCustomers(ctx context.Context) ([]Customer, error)
}
type repoSvc struct {
*Queries
db *sql.DB
}
func NewRepository(dbconn *sql.DB) Repository {
return &repoSvc{
Queries: New(dbconn),
db: dbconn,
}
}
customerRepo := customerRepo.NewRepository(conn)
GetCustomerById is the SQLC generated method
conn is the database connection
How to make the connection based on a parameter (from cookie or context)?
The simplest way, assuming you are using separate databases, is to maintain a map[tenantID]Repository, where tenantID is the way you differentiate between tenants (e.g. a string or uint that contains the tenant ID).
This way you can do everything at runtime:
when you need to add a tenant, just instantiate the Repository for that tenant and add it to the map
when you need to remove a tenant, just remove its Repository from the map and close the DB connection
when you need to perform a query for a tenant, lookup the corresponding Repository in the map, and use it to perform the query for that tenant
If the operations above may happen concurrently, make sure that you're using some synchronization mechanism to avoid data races when accessing the map (e.g. sync.Map, or sync.RWMutex).
If you have a database table that stores the tenants and their DB connection URIs, you can still use this approach: when you need to perform a query check if the Repository exists in the map: if it's missing, query the tenant table and add the Repository for that tenant to the map. You can then periodically scan the map and remove any Repository that has not been used for some time.
To make all of this easier you could also wrap the whole machinery into a MultitenantRepository interface, that is identical to the Repository interface but that accepts an additional tenantID parameter on each method:
type MultitenantRepository interface {
GetCustomerById(ctx context.Context, tenant tenantID, id int64) (Customer, error)
ListCustomers(ctx context.Context, tenant tenantID) ([]Customer, error)
}
This will avoid exposing all the complexity of your multitenant setup to your business logic.
In a Google App Engine app, I have this model:
#PersistenceCapable(identityType = IdentityType.APPLICATION)
public class Message {
#PrimaryKey
#Persistent(valueStrategy = IdGeneratorStrategy.IDENTITY)
private Long id;
#Persistent
private Date timestamp;
#Persistent
private String text;
#Unowned
#Persistent(defaultFetchGroup = "true")
private User sender;
...
}
The model has an #Unowned relation to a sender, since a user can exist independently of a message.
What I want to do is persist Message objects with partial User objects (e.g. I'm only interested in storing the user id and username). In my endpoint class I'm storing messages just fine, however, if I don't include all fields for the given user in the relationship, the user object is updated with the fields missing (e.g. user in question no longer has a password etc.). What is the best way of achieving what I want, without 'corrupting' the original object?
PS
My endpoints method is dead simple. Basically just calling pm.makePersistent(message); on the message (given as a method parameter).
You are using the #Unowned annotation which means that only a reference to the actual User entity will be stored in the sender variable under your Message class.
When you access the sender variable, the Datastore will execute a get query to retrieve the User entity that is linked to the message.
You can confirm this for yourself by navigating to your project's Datastore dashboard (https://console.cloud.google.com/datastore for production, and http://localhost:8080/_ah/admin for local) and browsing the Message entities.
You should see the field where the User is stored named something like user_id_OID with a value of Key(User/XXXXX).
As a side note, Google recommends moving away from JDO/JPA to Objectify or the Datastore API.
Warning: We think most developers will have a better experience using
the low-level Datastore API, or one of the open-source APIs developed
specifically for Datastore, such as Objectify. JDO was designed for
use with traditional relational databases, and so has no way to
explicitly represent some of the aspects of Datastore that make it
different from relational databases, such as entity groups and
ancestor queries. This can lead to subtle issues that are difficult to
understand and fix.
See here:
https://cloud.google.com/appengine/docs/java/datastore/jdo/overview-dn2
Some weeks ago I discovered OpenID Connect and IdentityServer V3. I ran some of the supplied examples and I am today at the point where I need to go further but I don't know how to proceed:
Actually we have an "home made" authentication and authorization process and we'd like to move to an OpenID Connect solution. Identity Server seems to be the perfect candidate to do this.
Today our users are stored into an SQL Server Database and ideally I'd like to "connect" this table to Identity Server (without touching to the schema of this table). I read about "MembershipReboot" but it uses its own Database. I also heard about making a custom user service but in the sample (CustomUserService) I did not find anything helpfull. Today I'am a little bit lost because I don't know where to go and I realize that I'am not very far from the target.
I need help
Thank you
In the Custom User Service Sample you mentioned, it includes three variations of user service to show different approaches, but you really only need one of them.
In the LocalRegistrationUserService sample you'll find lines like these:
public override Task AuthenticateLocalAsync(LocalAuthenticationContext context)
{
var user = Users.SingleOrDefault(x => x.Username == context.UserName && x.Password == context.Password);
/// snip ...
and these:
public override Task GetProfileDataAsync(ProfileDataRequestContext context)
{
// issue the claims for the user
var user = Users.SingleOrDefault(x => x.Subject == context.Subject.GetSubjectId());
/// snip...
You need to replace those calls which look up values from the in-memory Users collection with something that opens a connection to SQL server and looks them up there instead.
See the Custom User Service documentation for more methods supported, but those two (AuthenticateLocalAsync, GetProfileDataAsync) plus your SQL lookup are all you need to get started.
How do you get the System Permissions to be pulled?
I am working on building a Metadata deployment for our company, and part of the deployment includes creating several new profiles. We are trying to capture all of the permissions for these profiles, but can't seem to get the system permissions to match in our test runs.
I know that when you retrieve the profile object, what you get is dynamic, based on what other object you are retrieving. (see Chap 4 bullet 2)
The Following System Permissions are different between our source and destination environment
Customize Application
Manage Connections
Manage Custom Report Types
Manage Dashboards
Manage Public Documents
Manage Public List Views
Manage Public Reports
Manage Public Templates
Manage Translation
Manage Users
Modify All Data
Reset User Passwords and Unlock Users
+ Send Email
Transfer Record
View All Data
note: + indicates added in the destination. All others are lost.
In order to use the Metadata API - the user must have the "API Enabled" and the "Modify All Data" permissions.
I have a table in my SQL2008 DB for Users and one for Roles and then the UserRoles bridging table. I am at the point where I have to RoleProvider to work and have decorated some of my Actions with [Authorize(Roles = "Administrator,Developer")]
I actually build the navigation on my site per user so the RoleProvider is just to prevent a lower level user from getting the URL from his Admin buddy and going to a page that he is not supposed to.
We build the site navigation on a per user base and have a mapping between the user, his role and the pages that the role he is in is allowed to see. I just want to know if there is any way to change the [Authorize(Roles = "")] to get the list of roles with permission to that action dynamically from my database? That way I do not have to go decorate all actions that I have, it will just be pulled from the DB as if by magic.
A simple example will be appreciated, thank you.
Jack
I basically wrote my own CustomAuthorize class that inherits from AuthorizeAttribute and in the OnAuthorization I did the look-up for access. If the user does not have access I basically do:
filterContext.Result = new HttpUnauthorizedResult();
filterContext.Result = new RedirectResult("/accessDenied");
Works, and I decorate my methods with: [CustomAuthorize]