Importing Azure Active Directory groups on Terraform - azure-active-directory

Using the azuread provider from Terraform, I am trying to create groups reading a CSV file like this:
display_name
Group1
Group2
Group3
Reading it in a local variable:
locals {
departments = csvdecode(file("${path.module}/aad_departments.csv"))
}
# Create groups
resource "azuread_group" "groups" {
for_each = { for group in local.departments : group.display_name => group }
display_name = each.value.display_name
prevent_duplicate_names = true
}
But I would like to import an existing group, "Group2", that already exists. I have used this command:
terraform import azuread_group.groups xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
But when I plan and apply this terraform script it throws an error saying that the group already exist:
"To be managed via Terraform, this resource needs to be imported into the State. Please see the resource documentation for "azuread_group" for more information."
How can I import it?
Thank you very much,

because you are using a for_each meta-argument to loop.
I am expecting { for group in local.departments : group.display_name => group } would result in the below construct.
{
"group1" = {
"display_name" = "group1"
}
"group2" = {
"display_name" = "group2"
}
"group3" = {
"display_name" = "group3"
}
}
You also need to add either security_enabled or mail_enabled in your terraform code as per your requirements.
reference error message : "security_enabled": one ofmail_enabled,security_enabled must be specified
resource "azuread_group" "groups" {
for_each = { for group in local.departments : group.display_name => group }
display_name = each.value.display_name
prevent_duplicate_names = true
security_enabled = true
#### OR #####
# mail_enabled = true
}
Finally, you have to use the below three commands to import the 3 groups in your state file.
terraform import 'azuread_group.groups["group1"]' "<object_id>" # to import group1
terraform import 'azuread_group.groups["group2"]' "<object_id>" # to import group2
terraform import 'azuread_group.groups["group3"]' "<object_id>" # to import group3
Just for the safer side, you can use terraform console and verify the value of construct out of { for group in local.departments : group.display_name => group } and adapt the commands/code accordingly.
Hope it helped.

Related

Terraform dynamic group creation/loop issues

I've searched and played around quite a bit and I've not come across the solution.
I am trying to manage subscription providers and preview features via the "azurerm_resource_provider_registration" resource.
i've got it working fine if I want to manage just one provider with multiple sub features using the following:
tfvars file
provider_name = "Microsoft.Network"
provider_feature_name = {
feature1 = {
feature_name = "BypassCnameCheckForCustomDomainDeletion"
registered = true
}
feature2 = {
feature_name = "AllowTcpPort25Out"
registered = true
}
}
main.tf
resource "azurerm_resource_provider_registration" "provider_registration" {
name = var.provider_name
dynamic "feature" {
for_each = var.provider_feature_name
content {
name = feature.value.feature_name
registered = feature.value.registered
}
}
}
works great if I only ever want to manage one provider and it's features.
The problem comes when/if I want to add an additional "provider_name". I've tried a separate provider_name block but I keep getting a "unexpected block here" error. if I introduce a block like so;
vars.tf
provider_name = {
provider1 = {
provider_name = "Microsoft.Network" {
feature1 = {
feature_name = "test"
registered = true
}
}
}
provider2 = {
provider_name = "Microsoft.Storage" {
feature2 = {
feature_name = "test2"
registered = true
}
}
}
}
main.tf
resource "azurerm_resource_provider_registration" "provider_registration" {
for_each = var.provider_name
name = each.value.provider_name
dynamic "feature" {
for_each = var.provider_feature_name
content {
name = feature.value.feature_name
registered = feature.value.registered
}
}
I can get it loop but cannot get it to associate only feature1 to provider 1 etc as these features are exclusive to that provider. It associates feature1 to provider 1 & 2.
If I try to introduce a for_each or dynamic group for the "name" value, it comes up with "blocks of type provider not expected here" and/or "argument name is required but no definition was found"
In short, how can I get my main to loop over each provider_name and only associate the sub block of features to that provider (with potential for multiple features per provider type). is it just not possible for this type of resource? or am I just not understanding the loop/for_each documentation correctly.
any help is appreciated
thank you.
First we need to cleanup and optimize the input structure. I have speculated on what the values should be since there are two different hypothetical structures specified in the question, but the structure itself is accurate.
providers = {
"Microsoft.Network" = {
features = { "BypassCnameCheckForCustomDomainDeletion" = true }
}
"Microsoft.Storage" = {
features = { "AllowTcpPort25Out" = true }
}
}
Now we can easily utilize this structure with a for_each meta-argument in the resource.
resource "azurerm_resource_provider_registration" "provider_registration" {
for_each = var.providers
name = each.key
dynamic "feature" {
for_each = each.value.features
content {
name = feature.key
registered = feature.value
}
}
}
and this results in two provider registrations with the corresponding feature mapped to each.

Prevent duplicate field in database

I need to create an api to create an entity which must have a unique name in it's data.
exp:
{
name: "Reza",
...
}
The question is how to throw and error of that name is already taken.
I can handle it by reading the whole table and check if there is no object with that name and then create the entity, but what can I do with concurrency?
if someone else calls the create api in the same time and the same name it's not in db yet, then I can't notice it's a duplicate name, then we encounter a duplication.
The project is on a baas and the service uses a mongodb but i don't access it directly, and because of sharding I can't use unique indexing.
any workaround for this situations?
Try adding the already found names in the db in an array , and compare the name that is being inserted. I will post this code that checks wether the app name that is being inserted to the db if available and render it some where else , you can benefit from the concept.
const updateAppNames = async () => {
let apps = await purchaseFromAppObjectModel.find({}).sort({ name: 1 });
const availableApps = [];
for (var i = 0; i <= apps.length; i++) {
try {
const obj = apps[i];
const appName = obj.appName;
if (availableApps.includes(appName)) {
console.log("This app exists :", appName);
} else {
console.log("This app does not exist :", appName);
availableApps.push(appName);
console.log(availableApps);
}
} catch {
console.error();
}
}
return availableApps;
};
result :
This app does not exist : com.xxx
[ 'com.xxx' ]
This app does not exist : com.xxy
[ 'com.xxx', 'com.xxy' ]
This app exists : com.xxx
This app exists : com.xxx
This app does not exist : any game
[ 'com.xxx', 'com.xxy', 'any game' ]
This app exists : any game
This app exists : any game
This app exists : any game
This app exists : any game
This app does not exist : any.any.any
[
'com.xxx',
'com.xxy',
'any game',
'any.any.any'
]
This app exists : any.any.any
This app exists : any.any.any
This app exists : any.any.any

How set limit for nested field in AWS amplify DynamoDB schema?

I have next query
export const listCategorys = `query ListCategorys(
$filter: ModelCategoryFilterInput
$limit: Int
$nextToken: String
) {
listCategorys(filter: $filter, limit: $limit, nextToken: $nextToken) {
items {
id
name
words {
items {
id
en
ru
statusLearn
}
nextToken
}
}
nextToken
}
}
`;
I want use limit for nested element words and try get result with help next query
const listCats = await API.graphql(graphqlOperation(listCategorys, {limit:10, words:{limit:100}}));
but this query not work. How right build query?
This sounds very similar to Appsync & GraphQL: how to filter a list by nested value, in that the default Amplify codegen only generates top-level filters/limits.
There's a feature request around this here that you may want to upvote & comment on: https://github.com/aws-amplify/amplify-cli/issues/2311
Otherwise, there isn't a way to do this out-of-the-box yet without modifying the VTL templates.

Keycloak/OIDC : retrieve user groups attributes

I've extracted a user's groups information from the OIDC endpoint of Keycloak, but they don't come with the group ATTRIBUTES I defined (see Attributes tab into the group form, near Settings). Is there a claim to add to my request?
I'm using a RESTeasy client to reach Keycloak's admin API (had much better results than using the provided admin client, yet):
#Path("/admin/realms/{realm}")
public interface KeycloakAdminService {
#GET
#Path("/users/{id}/groups")
#Consumes(MediaType.APPLICATION_JSON)
#Produces(MediaType.APPLICATION_JSON)
List<GroupRepresentation> getUserGroups(#PathParam("realm") String realm, #PathParam("id") String userId,
#HeaderParam(AUTHORIZATION) String accessToken);
//DEBUG the access token must always be prefixed by "Bearer "
}
So I can fetch a user's groups:
private void fetchUserGroups(UserInfoOIDC infos, String userId) {
log.info("Fetching user groups from {}...", getRealm());
try {
KeycloakAdminService proxy = kcTarget.proxy(KeycloakAdminService.class);
AccessTokenResponse response = authzClient.obtainAccessToken(getAdminUsername(), getAdminPassword());
List<GroupRepresentation> groups = proxy.getUserGroups(getRealm(), userId,
"Bearer " + response.getToken());
infos.importUserGroups(groups); //DEBUG here we go!
} catch (WebApplicationException e) {
log.error("User groups failure on {}: {}", getRealm(), e.getMessage());
}
}
But when it comes to data exploration, it turns out that no attributes are provided into the GroupRepresentation#getAttributes structure.
I've read that claims can be added to user info requests. Does it work on the admin API? How can I achieve that result with RESTeasy templates?
Thx
I was able to achieve this by adding groups/roles info in token other claims property:
For this in keycloak config, go to your client -> mappers & add a group/role mapper. E.g.
Now this info will start coming in your access token:
To access these group attribute in Java you can extract it from otherclaims property of accesstoken. E.g.:
KeycloakSecurityContext keycloakSecurityContext = (KeycloakSecurityContext)(request.getAttribute(KeycloakSecurityContext.class.getName()));
AccesToken token = keycloakSecurityContext.getToken();
In below image you can see that otherclaims property of token is filled with groups attribute that we created on keycloak. Note that if we had named "token claim property" as groupXYZ, the otherclaims would be showing:
groupsXYZ=[Administrator]
This is how I could eventually map group attributes (inherited as user attributes, as suspected before) into user informations, into the "other claims" section :
It is possible to inherit attributes from the group by switching on Aggregate attribute values option during the creation of a new User Attribute mapper.
First of all I think the answers above are correct. I was able to achieve what I wanted to do by following recommendations from them.
But I have also broke by production keycloak integration with auth2-proxy which lead to some outage for internal users :)
So, I took time to investigate a bit and came up with creating new client scope and adding custom client role / realm role / group mappers to it.
It works, and also you don't break your working keycloak integrations with other services ;)
Here is all my terraform code which you can you to reproduce what I did:
variable "realm_name" {
type = string
description = "Name of the realm to create"
default = "master"
}
variable "keycloack_user" {
type = string
description = "Keycloak admin user"
default = "admin"
}
variable "keycloack_password" {
type = string
description = "Keycloak admin password"
default = "admin"
}
variable "keycloak_url" {
type = string
description = "Keycloak url"
default = "http://localhost:8090"
}
variable "oauth_fqdn" {
type = string
description = "FQDN of the oauth server used for valid redirects"
default = "http://localhost:3000/*"
}
terraform {
required_version = ">= 1.0.0"
required_providers {
keycloak = {
source = "mrparkers/keycloak"
version = ">= 3.7.0"
}
}
}
provider "keycloak" {
client_id = "admin-cli"
username = var.keycloack_user
password = var.keycloack_password
url = var.keycloak_url
realm = var.realm_name
# base_path = "/auth"
}
data "keycloak_realm" "realm" {
realm = var.realm_name
}
resource "keycloak_openid_client" "client" {
realm_id = data.keycloak_realm.realm.id
client_id = "my-client"
name = "my-client"
enabled = true
access_type = "CONFIDENTIAL"
valid_redirect_uris = [
var.oauth_fqdn
]
login_theme = "keycloak"
standard_flow_enabled = true
}
output "keycloak_client_id" {
value = keycloak_openid_client.client.client_id
}
output "keycloak_client_secret" {
value = keycloak_openid_client.client.client_secret
sensitive = true
}
// creating custom scope
resource "keycloak_openid_client_scope" "this" {
realm_id = data.keycloak_realm.realm.id
name = "group_and_roles"
description = "When requested, this scope will map a user's group memberships and all roles to a claim"
include_in_token_scope = true
}
// creating custom group mapper
resource "keycloak_generic_protocol_mapper" "groups" {
realm_id = data.keycloak_realm.realm.id
client_scope_id = keycloak_openid_client_scope.this.id
name = "groups mapper"
protocol = "openid-connect"
protocol_mapper = "oidc-group-membership-mapper"
config = {
"full.path" : "true",
"id.token.claim" : "true",
"access.token.claim" : "true",
"claim.name" : "groups",
"userinfo.token.claim" : "true"
}
}
// creating custom role mapper for realm level roles
resource "keycloak_generic_protocol_mapper" "realm_roles" {
realm_id = data.keycloak_realm.realm.id
client_scope_id = keycloak_openid_client_scope.this.id
name = "realm roles mapper"
protocol = "openid-connect"
protocol_mapper = "oidc-usermodel-realm-role-mapper"
config = {
"multivalued" : "true",
"userinfo.token.claim" : "true",
"id.token.claim" : "true",
"access.token.claim" : "true",
"claim.name" : "realm_roles",
"jsonType.label" : "String"
}
}
// creating custom role mapper for client level roles
resource "keycloak_generic_protocol_mapper" "client_roles" {
realm_id = data.keycloak_realm.realm.id
client_scope_id = keycloak_openid_client_scope.this.id
name = "client roles mapper"
protocol = "openid-connect"
protocol_mapper = "oidc-usermodel-client-role-mapper"
config = {
"multivalued" : "true",
"userinfo.token.claim" : "true",
"id.token.claim" : "true",
"access.token.claim" : "true",
"claim.name" : "client_roles",
"jsonType.label" : "String"
}
}
// adding custom scope to client as optional
resource "keycloak_openid_client_optional_scopes" "client_optional_scopes" {
realm_id = data.keycloak_realm.realm.id
client_id = keycloak_openid_client.client.id
optional_scopes = [
"address",
"phone",
"offline_access",
"microprofile-jwt",
keycloak_openid_client_scope.this.name
]
}
And later I setup my application level auth config as follows:
AUTH_ISSUER_URL=http://localhost:8090/realms/master
AUTH_CLIENT_ID=my-client
AUTH_CLIENT_SECRET=client-secret-get-it-from-output
AUTH_SCOPES="profile,email,openid,offline_access,group_and_roles"

Handling Users with MongoDB Stitch App within Atlas Cluster

I have an MongoDB Stitch app, that users the Email/Password authentication. This creates users within the Stitch App that I can authenticate on the page. I also have an MongoDB Atlas Cluster for my database. In the cluster I have a DB with the name of the project, then a collection underneath that for 'Matches'. So when I insert the 'Matches' into the collection, I can send the authenticated user id from Stitch, so that I have a way to query all Matches for a particular User. But how can I add additional values to the 'User' collection in stitch? That user section is sort of prepackaged in Stitch with whatever authentication type you choose (email/password). But for my app I want to be able to store something like a 'MatchesWon' or 'GamePreference' field on the 'User' collection.
Should I create a collection for 'Users' the same way I did for 'Matches' in my Cluster and just insert the user id that is supplied from Stitch and handle the fields in that collection? Seems like I would be duplicating the User data, but I'm not sure I understand another way to do it. Still learning, I appreciate any feedback/advice.
There isn't currently a way to store your own data on the internal user objects. Instead, you can use authentication triggers to manage users. The following snippet is taken from these docs.
exports = function(authEvent){
// Only run if this event is for a newly created user.
if (authEvent.operationType !== "CREATE") { return }
// Get the internal `user` document
const { user } = authEvent;
const users = context.services.get("mongodb-atlas")
.db("myApplication")
.collection("users");
const isLinkedUser = user.identities.length > 1;
if (isLinkedUser) {
const { identities } = user;
return users.updateOne(
{ id: user.id },
{ $set: { identities } }
)
} else {
return users.insertOne({ _id: user.id, ...user })
.catch(console.error)
}
};
MongoDB innovates at a very fast pace - and while in 2019 there wasn't a way to do this elegantly, now there is. You can now enable custom user data on MongoDB realm! (https://docs.mongodb.com/realm/users/enable-custom-user-data/)
https://docs.mongodb.com/realm/sdk/node/advanced/access-custom-user-data
const user = context.user;
user.custom_data.primaryLanguage == "English";
--
{
id: '5f1f216e82df4a7979f9da93',
type: 'normal',
custom_data: {
_id: '5f20d083a37057d55edbdd57',
userID: '5f1f216e82df4a7979f9da93',
primaryLanguage: 'English',
},
data: { email: 'test#test.com' },
identities: [
{ id: '5f1f216e82df4a7979f9da90', provider_type: 'local-userpass' }
]
}
--
const customUserData = await user.refreshCustomData()
console.log(customUserData);

Resources