Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow authentication using RBAC instead of API Keys #427

Closed
wants to merge 17 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .env.sample
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
AZURE_AUTH_TYPE=
AZURE_SEARCH_SERVICE=
AZURE_SEARCH_INDEX=
AZURE_SEARCH_KEY=
Expand Down
13 changes: 13 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,19 @@ To remove this restriction, you can add `AUTH_ENABLED=False` to the environment

To add further access controls, update the logic in `getUserInfoList` in `frontend/src/pages/chat/Chat.tsx`.

### Authenticate using Azure RBAC
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would rather the flow of the application take care of all the settings and not require the user to take manual action.

We are experimenting with adding an AZURE_AUTH_TYPE parameter that can get set to rbac or keys and the bicep does the appropriate thing based on that.

It's not finalized, but should be soon.

To authenticate using Azure RBAC instead of the default API Keys, rerun your deployment with `AZURE_AUTH_TYPE` set to
`rbac`. Alternatively, to make the changes manually, follow the steps below:
1. Ensure role assignments listed on [this page](https://learn.microsoft.com/azure/ai-services/openai/concepts/use-your-data?tabs=ai-search#azure-role-based-access-controls-azure-rbac-for-adding-data-sources)
have been created.
2. Navigate to your Search service in the Azure Portal
3. Under Settings, select `Keys`
4. Select either `Role-based access control` or `Both`
5. Navigate to your App service in the Azure Portal
6. Under Settings, select `Configuration`
7. Set the value of the `AZURE_AUTH_TYPE` setting to `rbac`
8. Restart the application

## Common Customization Scenarios
Feel free to fork this repository and make your own modifications to the UX or backend logic. For example, you may want to change aspects of the chat display, or expose some of the settings in `app.py` in the UI for users to try out different behaviors.

Expand Down
78 changes: 51 additions & 27 deletions app.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ def assets(path):
SEARCH_TOP_K = os.environ.get("SEARCH_TOP_K", 5)
SEARCH_STRICTNESS = os.environ.get("SEARCH_STRICTNESS", 3)
SEARCH_ENABLE_IN_DOMAIN = os.environ.get("SEARCH_ENABLE_IN_DOMAIN", "true")
AZURE_AUTH_TYPE = os.environ.get("AZURE_AUTH_TYPE", "keys")

# ACS Integration Settings
AZURE_SEARCH_SERVICE = os.environ.get("AZURE_SEARCH_SERVICE")
Expand Down Expand Up @@ -146,16 +147,22 @@ def assets(path):
logging.exception("Exception in CosmosDB initialization", e)
cosmos_conversation_client = None

def should_use_keys():
return AZURE_AUTH_TYPE == "keys"

def get_default_token():
return DefaultAzureCredential().get_token("https://cognitiveservices.azure.com/.default").token

def is_chat_model():
if 'gpt-4' in AZURE_OPENAI_MODEL_NAME.lower() or AZURE_OPENAI_MODEL_NAME.lower() in ['gpt-35-turbo-4k', 'gpt-35-turbo-16k']:
return True
return False

def should_use_data():
if AZURE_SEARCH_SERVICE and AZURE_SEARCH_INDEX and AZURE_SEARCH_KEY:
if AZURE_SEARCH_SERVICE and AZURE_SEARCH_INDEX:
if DEBUG_LOGGING:
logging.debug("Using Azure Cognitive Search")

return True

if AZURE_COSMOSDB_MONGO_VCORE_DATABASE and AZURE_COSMOSDB_MONGO_VCORE_CONTAINER and AZURE_COSMOSDB_MONGO_VCORE_INDEX and AZURE_COSMOSDB_MONGO_VCORE_CONNECTION_STRING:
Expand Down Expand Up @@ -249,29 +256,33 @@ def prepare_body_headers_with_data(request):
if DEBUG_LOGGING:
logging.debug(f"FILTER: {filter}")

body["dataSources"].append(
{
"type": "AzureCognitiveSearch",
"parameters": {
"endpoint": f"https://{AZURE_SEARCH_SERVICE}.search.windows.net",
"key": AZURE_SEARCH_KEY,
"indexName": AZURE_SEARCH_INDEX,
"fieldsMapping": {
"contentFields": parse_multi_columns(AZURE_SEARCH_CONTENT_COLUMNS) if AZURE_SEARCH_CONTENT_COLUMNS else [],
"titleField": AZURE_SEARCH_TITLE_COLUMN if AZURE_SEARCH_TITLE_COLUMN else None,
"urlField": AZURE_SEARCH_URL_COLUMN if AZURE_SEARCH_URL_COLUMN else None,
"filepathField": AZURE_SEARCH_FILENAME_COLUMN if AZURE_SEARCH_FILENAME_COLUMN else None,
"vectorFields": parse_multi_columns(AZURE_SEARCH_VECTOR_COLUMNS) if AZURE_SEARCH_VECTOR_COLUMNS else []
},
"inScope": True if AZURE_SEARCH_ENABLE_IN_DOMAIN.lower() == "true" else False,
"topNDocuments": int(AZURE_SEARCH_TOP_K),
"queryType": query_type,
"semanticConfiguration": AZURE_SEARCH_SEMANTIC_SEARCH_CONFIG if AZURE_SEARCH_SEMANTIC_SEARCH_CONFIG else "",
"roleInformation": AZURE_OPENAI_SYSTEM_MESSAGE,
"filter": filter,
"strictness": int(AZURE_SEARCH_STRICTNESS)
}
})
cognitiveSearchDataSource = {
sarah-widder marked this conversation as resolved.
Show resolved Hide resolved
"type": "AzureCognitiveSearch",
"parameters": {
"endpoint": f"https://{AZURE_SEARCH_SERVICE}.search.windows.net",
"indexName": AZURE_SEARCH_INDEX,
"fieldsMapping": {
"contentFields": parse_multi_columns(AZURE_SEARCH_CONTENT_COLUMNS) if AZURE_SEARCH_CONTENT_COLUMNS else [],
"titleField": AZURE_SEARCH_TITLE_COLUMN if AZURE_SEARCH_TITLE_COLUMN else None,
"urlField": AZURE_SEARCH_URL_COLUMN if AZURE_SEARCH_URL_COLUMN else None,
"filepathField": AZURE_SEARCH_FILENAME_COLUMN if AZURE_SEARCH_FILENAME_COLUMN else None,
"vectorFields": parse_multi_columns(AZURE_SEARCH_VECTOR_COLUMNS) if AZURE_SEARCH_VECTOR_COLUMNS else []
},
"inScope": True if AZURE_SEARCH_ENABLE_IN_DOMAIN.lower() == "true" else False,
"topNDocuments": int(AZURE_SEARCH_TOP_K),
"queryType": query_type,
"semanticConfiguration": AZURE_SEARCH_SEMANTIC_SEARCH_CONFIG if AZURE_SEARCH_SEMANTIC_SEARCH_CONFIG else "",
"roleInformation": AZURE_OPENAI_SYSTEM_MESSAGE,
"filter": filter,
"strictness": int(AZURE_SEARCH_STRICTNESS)
}
}

if should_use_keys():
cognitiveSearchDataSource["parameters"]["key"] = AZURE_SEARCH_KEY

body["dataSources"].append(cognitiveSearchDataSource)

elif DATASOURCE_TYPE == "AzureCosmosDB":
# Set query type
query_type = "vector"
Expand Down Expand Up @@ -359,9 +370,13 @@ def prepare_body_headers_with_data(request):

headers = {
'Content-Type': 'application/json',
'api-key': AZURE_OPENAI_KEY,
"x-ms-useragent": "GitHubSampleWebApp/PublicAPI/3.0.0"
}

if should_use_keys():
headers["api-key"] = AZURE_OPENAI_KEY
else:
headers["Authorization"] = "Bearer " + get_default_token()

return body, headers

Expand Down Expand Up @@ -546,7 +561,11 @@ def conversation_without_data(request_body):
openai.api_type = "azure"
openai.api_base = AZURE_OPENAI_ENDPOINT if AZURE_OPENAI_ENDPOINT else f"https://{AZURE_OPENAI_RESOURCE}.openai.azure.com/"
openai.api_version = "2023-08-01-preview"
openai.api_key = AZURE_OPENAI_KEY

if should_use_keys():
openai.api_key = AZURE_OPENAI_KEY
else:
openai.api_key = + get_default_token()

request_messages = request_body["messages"]
messages = [
Expand Down Expand Up @@ -900,7 +919,12 @@ def generate_title(conversation_messages):
openai.api_type = "azure"
openai.api_base = base_url
openai.api_version = "2023-03-15-preview"
openai.api_key = AZURE_OPENAI_KEY

if should_use_keys():
openai.api_key = AZURE_OPENAI_KEY
else:
openai.api_key = get_default_token()

completion = openai.ChatCompletion.create(
engine=AZURE_OPENAI_MODEL,
messages=messages,
Expand Down
5 changes: 5 additions & 0 deletions infra/core/ai/cognitiveservices.bicep
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ resource account 'Microsoft.CognitiveServices/accounts@2023-05-01' = {
publicNetworkAccess: publicNetworkAccess
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What updates are required? Do you just mean to pull from main?

}
sku: sku
identity: {
type: 'SystemAssigned'
}
}

@batchSize(1)
Expand All @@ -41,3 +44,5 @@ output id string = account.id
output name string = account.name
output skuName string = account.sku.name
output key string = account.listKeys().key1
output identityPrincipalId string = account.identity.principalId
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should match the name of the other core files when they provide an id, which is just principalId


6 changes: 4 additions & 2 deletions infra/core/search/search-services.bicep
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ param sku object = {

param authOptions object = {}
param semanticSearch string = 'disabled'
param authType string = 'rbac'
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What updates are required? Do you just mean to pull from main?


resource search 'Microsoft.Search/searchServices@2021-04-01-preview' = {
name: name
Expand All @@ -17,8 +18,8 @@ resource search 'Microsoft.Search/searchServices@2021-04-01-preview' = {
type: 'SystemAssigned'
}
properties: {
authOptions: authOptions
disableLocalAuth: false
authOptions: authType == 'keys' ? authOptions : null
disableLocalAuth: authType == 'keys' ? false : true
disabledDataExfiltrationOptions: []
encryptionWithCmk: {
enforcement: 'Unspecified'
Expand All @@ -41,3 +42,4 @@ output endpoint string = 'https://${name}.search.windows.net/'
output name string = search.name
output skuName string = sku.name
output adminKey string = search.listAdminKeys().primaryKey
output identityPrincipalId string = search.identity.principalId
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same comment about name principalId

75 changes: 71 additions & 4 deletions infra/main.bicep
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,12 @@ param appServicePlanName string = ''
param backendServiceName string = ''
param resourceGroupName string = ''

@allowed([
'keys'
'rbac'
])
param authType string = 'rbac'

param searchServiceName string = ''
param searchServiceResourceGroupName string = ''
param searchServiceResourceGroupLocation string = location
Expand Down Expand Up @@ -115,10 +121,11 @@ module backend 'core/host/appservice.bicep' = {
authClientId: authClientId
authIssuerUri: authIssuerUri
appSettings: {
AZURE_AUTH_TYPE: authType
// search
AZURE_SEARCH_INDEX: searchIndexName
AZURE_SEARCH_SERVICE: searchService.outputs.name
AZURE_SEARCH_KEY: searchService.outputs.adminKey
AZURE_SEARCH_KEY: authType == 'keys' ? searchService.outputs.adminKey : null
AZURE_SEARCH_USE_SEMANTIC_SEARCH: searchUseSemanticSearch
AZURE_SEARCH_SEMANTIC_SEARCH_CONFIG: searchSemanticSearchConfig
AZURE_SEARCH_TOP_K: searchTopK
Expand All @@ -131,7 +138,7 @@ module backend 'core/host/appservice.bicep' = {
AZURE_OPENAI_RESOURCE: openAi.outputs.name
AZURE_OPENAI_MODEL: openAIModel
AZURE_OPENAI_MODEL_NAME: openAIModelName
AZURE_OPENAI_KEY: openAi.outputs.key
AZURE_OPENAI_KEY: authType == 'keys' ? openAi.outputs.key : null
AZURE_OPENAI_TEMPERATURE: openAITemperature
AZURE_OPENAI_TOP_P: openAITopP
AZURE_OPENAI_MAX_TOKENS: openAIMaxTokens
Expand Down Expand Up @@ -189,6 +196,7 @@ module searchService 'core/search/search-services.bicep' = {
aadAuthFailureMode: 'http401WithBearerChallenge'
}
}
authType: authType
sku: {
name: !empty(searchServiceSkuName) ? searchServiceSkuName : 'standard'
}
Expand Down Expand Up @@ -250,6 +258,27 @@ module searchServiceContribRoleUser 'core/security/role.bicep' = {
}
}


module openAiContributorRoleUser 'core/security/role.bicep' = if (authType == 'rbac') {
scope: searchServiceResourceGroup
name: 'openai-contributor-role-user'
params: {
principalId: principalId
roleDefinitionId: 'a001fd3d-188f-4b5d-821b-7da978bf7442'
principalType: 'User'
}
}

module cognitiveServicesContributorRoleUser 'core/security/role.bicep' = if (authType == 'rbac') {
scope: searchServiceResourceGroup
name: 'cognitive-services-contributor-role-user'
params: {
principalId: principalId
roleDefinitionId: '25fbc0a9-bd7c-42a3-aa1a-3b75d497ee68'
principalType: 'User'
}
}

// SYSTEM IDENTITIES
module openAiRoleBackend 'core/security/role.bicep' = {
scope: openAiResourceGroup
Expand All @@ -271,6 +300,46 @@ module searchRoleBackend 'core/security/role.bicep' = {
}
}

module searchRoleOpenAi 'core/security/role.bicep' = if (authType == 'rbac') {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any new roles you create will also need to be assigned to the user's principalId, see "user roles" above.

scope: searchServiceResourceGroup
name: 'search-role-openai'
params: {
principalId: openAi.outputs.identityPrincipalId
roleDefinitionId: '1407120a-92aa-4202-b7e9-c0e197c71c8f'
principalType: 'ServicePrincipal'
}
}

module searchServiceRoleOpenAi 'core/security/role.bicep' = if (authType == 'rbac') {
scope: searchServiceResourceGroup
name: 'search-service-role-openai'
params: {
principalId: openAi.outputs.identityPrincipalId
roleDefinitionId: '7ca78c08-252a-4471-8644-bb5ff32d4ba0'
principalType: 'ServicePrincipal'
}
}

module openAiContributorRoleSearch 'core/security/role.bicep' = if (authType == 'rbac') {
scope: searchServiceResourceGroup
name: 'openai-contributor-role-search'
params: {
principalId: searchService.outputs.identityPrincipalId
roleDefinitionId: 'a001fd3d-188f-4b5d-821b-7da978bf7442'
principalType: 'ServicePrincipal'
}
}

module cognitiveServicesContributorRoleSearch 'core/security/role.bicep' = if (authType == 'rbac') {
scope: searchServiceResourceGroup
name: 'cognitive-services-contributor-role-search'
params: {
principalId: searchService.outputs.identityPrincipalId
roleDefinitionId: '25fbc0a9-bd7c-42a3-aa1a-3b75d497ee68'
principalType: 'ServicePrincipal'
}
}

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In outputs below we shouldn't be outputting keys. You will get a bicep linter warning. Best to ask the user to manually set them with in env vars at this time until we have a secure way to store them.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Phew, glad to see the keys are out of this output finally! That's been a TODO (in my head) for a long time.

Another option btw is to provide a script that grabs the keys, which is what I've had to do for another repo which unfortunately requires keys:
https://github.com/Azure-Samples/contoso-chat/pull/33/files#diff-c8719e3915d76118aeee543ed85367b315252d46ef1d793635a82eaef96ce7b4R19

But probably okay not to do that here, since we now have the keyless option.

// For doc prep
module docPrepResources 'docprep.bicep' = {
name: 'docprep-resources${resourceToken}'
Expand All @@ -297,7 +366,6 @@ output AZURE_SEARCH_INDEX string = searchIndexName
output AZURE_SEARCH_SERVICE string = searchService.outputs.name
output AZURE_SEARCH_SERVICE_RESOURCE_GROUP string = searchServiceResourceGroup.name
output AZURE_SEARCH_SKU_NAME string = searchService.outputs.skuName
output AZURE_SEARCH_KEY string = searchService.outputs.adminKey
output AZURE_SEARCH_USE_SEMANTIC_SEARCH bool = searchUseSemanticSearch
output AZURE_SEARCH_SEMANTIC_SEARCH_CONFIG string = searchSemanticSearchConfig
output AZURE_SEARCH_TOP_K int = searchTopK
Expand All @@ -314,7 +382,6 @@ output AZURE_OPENAI_ENDPOINT string = openAi.outputs.endpoint
output AZURE_OPENAI_MODEL string = openAIModel
output AZURE_OPENAI_MODEL_NAME string = openAIModelName
output AZURE_OPENAI_SKU_NAME string = openAi.outputs.skuName
output AZURE_OPENAI_KEY string = openAi.outputs.key
output AZURE_OPENAI_EMBEDDING_NAME string = '${embeddingDeploymentName}'
output AZURE_OPENAI_TEMPERATURE int = openAITemperature
output AZURE_OPENAI_TOP_P int = openAITopP
Expand Down
3 changes: 3 additions & 0 deletions infra/main.parameters.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,9 @@
},
"authClientSecret": {
"value": "${AUTH_CLIENT_SECRET}"
},
"authType": {
"value": "${AZURE_AUTH_TYPE=rbac}"
}
}
}